资讯详情

linux系统编程(十四):线程同步全景

📅 2026/10/9 5:26:14 | 华诺云谱 👁 阅读
linux系统编程(十四):线程同步全景
线程同步全景 —— 互斥 协作 锁的三件神奇事多线程编程最大的迷雾不是会用什么 API而是我到底想解决什么问题。这篇按为什么要同步 → 同步本质是两件事 → 锁做了哪三件事 → 为什么单靠锁不够 → 各种原语怎么选五步讲清楚。读完之后再写多线程每个pthread_mutex_lock你都知道为啥要锁。0. 引言30 行代码 100 个 bugintbalance1000;void*transfer(void*arg){for(inti0;i100000;i){balance100;// ⛔ 看似一行实际 3 步}returnNULL;}intmain(void){pthread_tt1,t2;pthread_create(t1,NULL,transfer,NULL);pthread_create(t2,NULL,transfer,NULL);pthread_join(t1,NULL);pthread_join(t2,NULL);printf(%d\n,balance);// 预期 21000000实际跑出来可能是 13283400 之类}balance 100不是原子的。两个线程交错执行 → 数据丢失。这就是数据竞争data race全部线程同步问题的源头。1. 先把线程同步拆成两件事很多人混在一起讲导致永远讲不清。本质是两个独立问题问题一句话定义例子互斥mutual exclusion同一时刻只允许一个线程进入临界区两个线程同时给银行账户 100结果不能丢失协作coordination / cooperation一个线程等另一个线程产生某个状态 / 事件消费者要等生产者把数据放进队列才能取锁mutex只解决互斥不解决协作。很多 bug 的根源就是用错了工具 —— 用锁去做协作于是退化成忙等 sleep硬轮询浪费 CPU 还不可靠。记住这个心智模型下面所有内容围绕它展开。2. 锁到底做了什么 —— 不止互斥两个字锁实际做了三件事缺一不可互斥访问临界区— 大家都知道的一件内存可见性屏障— 经常被忽略的一件防止编译器 / CPU 重排序— 几乎没人提的一件2.1 ① 互斥访问最直观的同一时间只能一个线程持有锁另一个lock()时被阻塞。pthread_mutex_tmPTHREAD_MUTEX_INITIALIZER;voidtransfer(void){pthread_mutex_lock(m);balance100;// 临界区pthread_mutex_unlock(m);}锁住后两个线程串行进入读 加 写原子化。2.2 ② 内存可见性这件最重要没有锁的情况下线程 A 修改了变量 x线程 B不一定能看到—— 因为A 修改 x 可能只写到自己的 L1 cache没刷到主内存B 读 x 可能从自己的 L1 cache 读到旧值。lock/unlock内部隐含 memory barrierlock()之后的读 → 强制看到主内存的最新值unlock()之前的写 → 强制刷到主内存这就是happens-before 关系这才是锁实现同步的核心机制。光说互斥是不够的没有内存可见性保证多核上的互斥也没意义。2.3 ③ 防止重排序编译器和 CPU 都会把指令重新排序优化。例如readyfalse;data42;readytrue;// 编译器可能把这两行交换如果交换了线程 B 看到ready true时data可能还是旧值。锁内部的 barrier 同时是compiler barrier CPU barrier禁止跨 lock/unlock 边界重排序所以锁不仅是等还是程序顺序的屏障。3. 锁能起到同步的作用吗 —— 只能起部分作用严格说同步语义锁能做吗怎么做互斥✅ 可以直接用内存可见性✅ 可以lock/unlock 隐含 barrier“线程 A 必须等线程 B 完成 XX 才能继续”❌ 单靠锁做不到需要条件变量或其他原语3.1 关键反例用锁实现等queueintq;mutex m;// 消费者while(true){lock_guardmutexlk(m);if(!q.empty()){intvq.front();// 处理}// 队列空怎么办放开锁循环再试 → 忙等CPU 100%}改成sleep_for(1ms)也只是缓解不是真正的同步 —— 存在唤醒延迟、CPU 浪费、唤醒丢失等问题。3.2 正确做法condition variablequeueintq;mutex m;condition_variable cv;// 生产者{lock_guardmutexlk(m);q.push(42);cv.notify_one();// 通知消费者有货了}// 消费者{unique_lockmutexlk(m);cv.wait(lk,[]{return!q.empty();});// 没货就睡有货被叫醒intvq.front();q.pop();}cv.wait内部做了三件神奇事释放锁把当前线程挂起不占 CPU被 notify 后重新拿锁这才是协作型同步。条件变量必须配 mutex 用缺一不可。3.3 cv 的 pthread C 接口#includepthread.hpthread_mutex_tmPTHREAD_MUTEX_INITIALIZER;pthread_cond_tcvPTHREAD_COND_INITIALIZER;intready0;// 等的一方pthread_mutex_lock(m);while(!ready)// ⭐ while 不是 if防虚假唤醒pthread_cond_wait(cv,m);do_work();pthread_mutex_unlock(m);// 通知方pthread_mutex_lock(m);ready1;pthread_cond_signal(cv);// 或 pthread_cond_broadcast 唤醒所有pthread_mutex_unlock(m);⚠️ 必须while不是ifcv 可能虚假唤醒spurious wakeup醒了要重新检查条件。⚠️pthread_cond_signal在 mutex 内还是外都能调用工程上通常放在 mutex 内—— 行为更直观。4. 线程同步原语全景表掌握了互斥 协作 三件神奇事心智模型后看这张表会很清晰原语解决问题用法关键词mutex互斥 内存可见性lock/unlockspinlock极短临界区的互斥不睡眠中断处理、内核态condition_variable等待某个条件满足wait/notifysemaphore计数信号量资源池acquire/releasebarrierN 个线程会合后一起继续arrive_and_waitlatch一次性会合点count_down/waitatomicT无锁同步CASload/store/compare_exchangefuture / promise异步结果传递get/set_valueread_write_lock多读单写shared_lock/unique_lockmemory_order_*内存模型微调acquire/release/seq_cst下面挨个补充用法。4.1 mutex —— 互斥锁最常用详见第 2 篇。pthread_mutex_tmPTHREAD_MUTEX_INITIALIZER;pthread_mutex_lock(m);/* 临界区 */pthread_mutex_unlock(m);变种变种行为PTHREAD_MUTEX_RECURSIVE同一线程可重入 lock 多次PTHREAD_MUTEX_ERRORCHECK检查死锁 / 重复 unlockPTHREAD_MUTEX_ROBUST持有者死了能恢复详见第 15 篇进程同步4.2 spinlock —— 自旋锁不让出 CPUCAS 自旋等锁。临界区极短 几百纳秒时比 mutex 划算否则浪费 CPU。pthread_spinlock_ts;pthread_spin_init(s,PTHREAD_PROCESS_PRIVATE);pthread_spin_lock(s);/* 极短临界区 */pthread_spin_unlock(s);用户态很少用。常见于内核中断 / 实时系统。4.3 cond —— 条件变量讲过了。等待某个条件成立的标准方式。4.4 semaphore —— 计数信号量#includesemaphore.hsem_ts;sem_init(s,0,5);// 初值 5pshared0 线程内sem_wait(s);// P 操作减 10 时阻塞/* 用资源 */sem_post(s);// V 操作加 1典型场景限流“同时最多 N 个工作线程”资源池连接池、对象池生产消费empty / full 两个 sem 配 mutex4.5 barrier —— N 线程会合pthread_barrier_tb;pthread_barrier_init(b,NULL,4);// 4 个线程void*worker(void*arg){do_phase1();pthread_barrier_wait(b);// 4 个都到了一起继续do_phase2();}并行计算分阶段常用阶段 1 全部线程跑完才能进阶段 2。4.6 latch —— 一次性会合C20barrier 是可重用的latch 用一次就废。简单场景比 barrier 易用。C 没标准 latch自己用 atomic 实现atomic_int countN;// 每个工作完成后atomic_fetch_sub(count,1);// 等所有工作完成while(atomic_load(count)0)/* spin or sleep */;4.7 atomic —— 无锁同步#includestdatomic.hatomic_int counter0;atomic_fetch_add(counter,1);// 原子加atomic_store(flag,1);intvatomic_load(counter);atomic_compare_exchange_weak(p,expected,desired);// CAS特点项特性说明✅不阻塞纳秒级比 mutex 快 1-2 个数量级✅适合简单计数器、ready 标志单变量原子操作✅隐含 memory barrier默认seq_cst内存序❌复杂结构难写无锁队列容易踩坑ABA / 顺序 / 重排详见第 2 篇。4.8 future / promiseC / Java 主流C 没标准 future但 pthread 能仿typedefstruct{pthread_mutex_tm;pthread_cond_tcv;intready;void*value;}future_t;voidpromise_set(future_t*f,void*v){pthread_mutex_lock(f-m);f-valuev;f-ready1;pthread_cond_broadcast(f-cv);pthread_mutex_unlock(f-m);}void*future_get(future_t*f){pthread_mutex_lock(f-m);while(!f-ready)pthread_cond_wait(f-cv,f-m);void*vf-value;pthread_mutex_unlock(f-m);returnv;}异步结果传递的标准抽象。4.9 read_write_lock —— 读多写少pthread_rwlock_trwPTHREAD_RWLOCK_INITIALIZER;pthread_rwlock_rdlock(rw);// 共享读多线程可同时pthread_rwlock_wrlock(rw);// 独占写pthread_rwlock_unlock(rw);读多写少的场景比 mutex 性能高。配置缓存、路由表、字典都常用。4.10 memory_order_* —— 微调内存模型atomic_store_explicit(x,1,memory_order_release);intyatomic_load_explicit(x,memory_order_acquire);可选值从松到严order含义relaxed仅保证原子性不限制重排consume数据依赖已废弃少用acquire后续读写不能跨该 load 重排到前release前面读写不能跨该 store 重排到后acq_relacquire releaseseq_cst顺序一致默认最严seq_cst性能最差但最安全。优化成 acquire/release 比较微妙90% 场景默认 seq_cst 就好。5. 选型决策树6. 常见误用速查6.1 用锁做协作 → 忙等while(1){lock(m);if(data_ready)break;unlock(m);/* 又抢锁、又释放 */// ⛔ CPU 100%}✅ 改用 cond。6.2 cond 用 if 不用 whilepthread_mutex_lock(m);if(!ready)pthread_cond_wait(cv,m);// ⛔ 虚假唤醒就出锅pthread_mutex_unlock(m);✅ 永远while (!ready) cond_wait(...)。6.3 atomic 不能保证复合操作原子atomic_int x;if(atomic_load(x)0){atomic_store(x,1);// ⛔ 中间可能被打断}✅ 用compare_exchangeintexpected0;atomic_compare_exchange_strong(x,expected,1);6.4 死锁加锁顺序不一致线程 A:lock(m1);lock(m2);线程 B:lock(m2);lock(m1);// 互相等防御全局规定一个加锁顺序所有代码遵守。或用pthread_mutex_trylock 超时退避。6.5 cond_signal 后立即销毁pthread_mutex_lock(m);ready1;pthread_cond_signal(cv);pthread_mutex_unlock(m);pthread_cond_destroy(cv);// ⛔ 等的线程还没醒✅ 等所有等待者都醒了再 destroy。或者用 join 同步。6.6 锁粒度太粗 / 太细锁粒度的核心不是“越细越好”而是让正确性、性能和维护成本取得平衡。粒度典型做法优点风险适合场景太粗所有线程都抢同一把大锁实现简单不容易写错锁竞争严重性能容易卡在一个临界区初版实现、并发压力不高的路径适中按对象、队列、分片或业务域拆锁性能和复杂度比较平衡需要明确锁顺序和数据归属大多数工程场景的推荐状态太细每个字段甚至每个小状态一把锁理论并发度高死锁风险大维护困难加锁开销可能超过收益只有在 profile 证明瓶颈后才考虑决策问题建议还没证明有性能瓶颈先用粗一点的锁优先保证正确性和可读性profile 显示锁竞争严重按热点对象、队列、分片逐步拆锁拆锁后出现死锁风险统一全局加锁顺序必要时使用trylock 退避临界区非常小但频繁先减少临界区内容再评估 atomic / lock-free而不是一上来就细拆经验口诀先粗后细先正确后优化只有 profile 证明锁竞争严重时才拆锁、分片或引入更复杂的无锁方案。7. 一个完整生产-消费例子#includepthread.h#includestdio.h#includestdlib.h#defineBUF_SIZE16typedefstruct{intdata[BUF_SIZE];inthead,tail;intcount;pthread_mutex_tm;pthread_cond_tnot_empty;pthread_cond_tnot_full;}ring_t;voidring_init(ring_t*r){r-headr-tailr-count0;pthread_mutex_init(r-m,NULL);pthread_cond_init(r-not_empty,NULL);pthread_cond_init(r-not_full,NULL);}voidring_push(ring_t*r,intv){pthread_mutex_lock(r-m);while(r-countBUF_SIZE)pthread_cond_wait(r-not_full,r-m);// 满了等r-data[r-tail]v;r-tail(r-tail1)%BUF_SIZE;r-count;pthread_cond_signal(r-not_empty);// 通知非空pthread_mutex_unlock(r-m);}intring_pop(ring_t*r){pthread_mutex_lock(r-m);while(r-count0)pthread_cond_wait(r-not_empty,r-m);// 空了等intvr-data[r-head];r-head(r-head1)%BUF_SIZE;r-count--;pthread_cond_signal(r-not_full);// 通知非满pthread_mutex_unlock(r-m);returnv;}包含本篇所有要点✅ mutex 保互斥✅ 两个 cond 解决等空 / 等满两种协作✅ wait 配 while 防虚假唤醒✅ signal 在 mutex 内调用行为可预测✅ 内存可见性靠 lock/unlock 自动保证8. 总结 —— 一张图记住整个体系线程同步的本质线程同步 互斥 协作互斥 (独占资源)协作 (等条件 / 通知)mutex / spinlockcond mutex (最通用)atomic (无锁)semaphore (计数 / 限流)rwlock (读多写少)barrier / latch (会合)future / promise (异步)锁的隐藏价值: 内存可见性 (barrier) 防重排序.口诀互斥用 mutex / atomic协作用 cond / sem锁的真正价值在 barrier不止于独占cv.wait 必配 while防虚假唤醒加锁顺序全局一致防死锁锁粒度先粗后细出问题再拆金句搞清互斥和协作是两件事是写好多线程代码的第一步。把锁当万能工具去做等 → CPU 100% 是必然结果。该用 cond 时用 cond。
📝

华诺云谱内容团队

资深建站顾问 · 行业研究员

10年+企业数字化服务经验,专注智能建站、SEO优化与品牌营销,持续输出建站技巧、行业洞察与营销干货,已帮助5000+企业实现数字化增长。

你可能需要的服务

订阅华诺云谱资讯周报

每周一封,精选建站技巧、SEO与营销干货,直达邮箱。已有 8,000+ 企业主订阅,助你少走弯路。

↑