线程同步 — 互斥锁、条件变量、屏障与读写锁¶
这一章讲什么: 线程共享内存,爽;线程同时改内存,炸。 第 12 章的信号量管进程,本章的 Pthreads 工具管线程——四件套: 互斥锁(一次一人)、条件变量(睡了等通知,别空转)、 屏障(人到齐了一起走)、读写锁(读者并行、写者独占)。 每个工具都配一个项目,最后你会拿到多线程编程的四条纪律。
1. 顶层全景:四个工具各管一件事¶
问题:多个线程改同一份数据 → 竞争条件(第 12 章的 counter++ 三指令交错)
│
mutex:把「读-改-写」夹成临界区,一次只有一个线程在里面
│ 与二元信号量的关键区别:只有锁住者能解锁
▼
新问题:条件不满足时怎么办?mutex 只能空转轮询 → 烧 CPU
│
条件变量:wait 原子地「放锁 + 睡到条件变量的队列」,
别人 signal 时醒来、重新拿锁、重验条件
▼
新问题:N 个线程分头干完各自一段,要一起进入下一阶段
│
屏障:pthread_barrier_wait,凑够 count 个线程齐放行
▼
新问题:数据结构「读多写少」,mutex 让读也互斥 → 太严
│
读写锁:读锁共享(多个读者同时)、写锁独占(一人写全员等)
图说:四件套是一条「问题逼出工具」的链。 先立本章的总纲 (原书开篇):多线程程序的正确性分两半——safety(坏事 不发生:数据不被写坏)与 liveness(好事终发生:不死锁、 不饿死、不活锁)1。
2. 互斥锁:mutex¶
声明即初始化:pthread_mutex_t m = PTHREAD_MUTEX_INITIALIZER;
(静态、默认属性)或 pthread_mutex_init(&m, NULL)(动态、
带错误检查)。初始为未锁2。三板斧:
pthread_mutex_lock(拿不到就睡)、pthread_mutex_trylock
(拿不到返 EBUSY 不睡)、pthread_mutex_unlock3。
normal 型 mutex 的语义五条:未锁→获得并返回 0;已锁→阻塞; 自己锁自己 = 死锁;解别人的锁/解未锁的 = 未定义行为; 被信号打断的等待会接着等4。与二元信号量的一处关键区别 (原书 NOTE):mutex 只有持有它的线程能解锁;信号量谁都能 post——用途不同5。
军规(原书边框):临界区越短越好、数量越少越好——mutex 把代码串行化了,等锁的线程全在浪费生命;加锁解锁本身也有 开销6。
四种类型:NORMAL(上面这套)、ERRORCHECK(开发期 用:重锁返 EDEADLK 而不是真死锁)、RECURSIVE(递归函数 用:内部计数器,重锁加一、解锁减一,归零才真放)、DEFAULT (同 NORMAL)7。
项目:threaded_progbar——第 9 章进度条的线程版¶
四计算线程共推 fraction_completed(共享 double = 临界区),
一把 frac_mutex 夹住「算增量→加→裁剪到 1.0→写日志文件」,
每线程的睡眠时长与自己的增量成正比(负载自然均衡:实测
26.3%/24.3%/24.5%/24.9%);taskorder 日志(O_APPEND 打开,
锁内写只为保持获得锁的顺序)事后用 awk 统计各线程拿锁
次数 23/24/18/19——交错可见8。两个细节都是本章的考点:
① drand48 不是线程安全函数(man 页 ATTRIBUTES 节明写),
改用 random();② SIGALRM 按第 15 章处方处理:main 先挡
信号,专线程 sigwait 收 SIGALRM → 调 refresh_progressbar,
收到终止信号则 pthread_cancel 杀光计算线程9。
3. 条件变量:睡了等,别空转¶
mutex 解决「别同时进」,解决不了「等条件」——用 mutex 写 生产者-消费者,消费者只能 lock→查 count→unlock→再 lock 的空转轮询(busy waiting)10。条件变量(pthread_cond_t) 与两样东西三元组配套:一个布尔条件(通常含共享变量) + 一个串行化测试该条件的 mutex。关联没有代码强制,是程序员 的纪律11。
pthread_cond_wait(&cond, &mutex) 的原子两步:调用前必须
已持有 mutex → 放锁 + 入睡眠队列一气呵成(中间窗口里
别人拿到锁再 signal,它醒来照样能赶上——不会丢);返回时
mutex 又回到它手里。pthread_cond_timedwait 多一个
绝对超时(ETIMEDOUT)。signal 唤醒一个,broadcast
唤醒全部12。
三条铁律:
- wait 必须放在 while 里,不许用 if——存在虚假唤醒 (spurious wakeup):signal 可能意外唤醒多个;POSIX 明说 「修这个问题会降低并发度」所以不修。醒来 ≠ 条件为真, 必须重新检查13;
- 信号不记账:signal 先于 wait 到达 = 丢了,没人醒;
- 一个条件变量同时只绑定一个 mutex(有人睡在里面时换 mutex 会失败)14。
生产者的标准写法(背下来):
pthread_mutex_lock(&buf_mutex);
while ( count == BUF_SIZE ) /* while,不是 if! */
pthread_cond_wait(&bufspace_available, &buf_mutex);
buf[rear] = data; rear = (rear+1) % BUF_SIZE; count++;
pthread_mutex_unlock(&buf_mutex);
pthread_cond_signal(&data_available); /* 通知消费者 */
4. 屏障:人到齐再走¶
pthread_barrier_init(&b, NULL, count) 设定「凑够几人」;
每人调 pthread_barrier_wait(&b)——第 count 个到达时全员
放行,屏障自动重置。返回值有花样:恰好一个线程拿到
PTHREAD_BARRIER_SERIAL_THREAD,其余拿 0——需要「齐步后
由一人做汇总」时就用它15。
项目:vmem_usage(屏障并行求和)。需求:统计全系统进程 的虚拟内存总量(第 10 章 spl_ps 的数据 × 第 7 章的目录遍历)。 设计权衡讲得极好:① 每加一个数就 lock 一次的全局 total = 把求和串行化,比单线程还慢;② 每线程算自己那段的部分和、 最后一次性汇总 = 正解(N/p 段,临界区只碰一次);③ 分轮 树形归并(barrier 分批汇合)适合段数极多的场合16。
5. 读写锁:读者并行、写者独占¶
场景:大数据结构,读频繁、偶尔写。mutex 太严(读也互斥), 读写锁(pthread_rwlock_t)三态:读锁(N 人可同时)、 写锁(一人独占)、未锁17。
API 对称:pthread_rwlock_rdlock / tryrdlock / timedrdlock
(读)、pthread_rwlock_wrlock / trywrlock / timedwrlock
(写)、pthread_rwlock_unlock(一个解锁函数通吃)、
PTHREAD_RWLOCK_INITIALIZER 静态初始化18。原书的
「房间与守卫」比方:读者进门留守卫在门房——新读者放不放行
取决于实现(reader-preference 配置下新读者能插队,可能
饿死写者;writer-preference 反之)19。
锁的语义还有几条暗礁,原书单列了一节「Further Details」, 都是会真咬人的20:
| 情形 | 结果 |
|---|---|
| 线程已持读锁,再 rdlock 同一把锁 | 允许(类递归锁),但 unlock 次数要与之配对 |
| 线程已持读锁或写锁,再 wrlock 升级 | 死锁,或失败返 EDEADLK |
| 持写锁的线程中途死掉 | 别人再拿锁:永远死等 |
| 持读锁的线程中途死掉 | 系统自动释放读锁 |
项目:pthread_rwlock_demo——把锁做成活体解剖¶
目的有二:练 rwlock,以及造一台能观察「读者/写者数量与偏好
如何影响临界区访问」的仪器21。结构:N 个读者线程 + M 个
写者线程,共享一个从文本文件载入的字符串数组;写者抢到写锁
后随机改约一半条目——只改元数据(自己的 ID 取负 + 改写
时间戳,文本不动),好让事后解剖容易;读者读到哪行就印哪行
(Read by 7: -9 3765.99… [116] spruce:读者 7 读到写者 9
改过的第 116 行)。两个观察装置:rwlockdemo.log 记每次
加锁/解锁的授予顺序;MONITOR 编译开关附加监控代码21。
- 起跑用屏障拉平。 不按赛马式起跑设计的话,先创建的 线程总是先拿到锁——若恰好是写者,读者从开场就被饿死, 实验数据没意义。所以每个 reader/writer 的 start 函数 开头都先过一道 barrier。
- 防写者饿死靠锁属性,但有前提。 默认(不给 -R 时)
给锁设
PTHREAD_RWLOCK_PREFER_WRITER_NONRECURSIVE_NP属性防写者饿死——「NONRECURSIVE」不是白写的:一旦有 读者递归持锁,开这个属性就是死锁;且写者太多时读者 会反过来饿死,所以这开关只适合写者不多的场合。
解剖手法照抄即可:./pthread_rwlock_demo -r12 -w3 treenames > run1
然后 grep 'scarlet oak' run1——能看出某行被哪几个写者改过、
间隔是不是一秒出头(写者释放后睡 1 秒,时间戳上的迟滞就是
它等锁等了多久);rwlockdemo.log 则给出锁的真实授予序列,
与 stdout 互相印证22。
如果错,会错在: 把 rwlock 当递归锁使(读者递归持读锁 本身合法,但配了解锁计数又开了写者优先属性,死锁等着你); 拿着读锁想「顺手升级成写锁」——原书明写这条路要么死锁要么 EDEADLK,升级只能先全放掉再重抢20。
6. 判断/边界/可带走¶
作者的判断: 「锁要短、条件变量 wait 要 while、库函数要查 ATTRIBUTES 节」三条军规贯穿全章;信号处方继续沿用 Kerrisk69。 rwlock 的「升级即死锁」与写者优先属性的非递归前提,是少见的 明写暗礁2022。边界: spinlock、条件变量属性没展开; 死锁的银行家算法类内容只在习题里1;rwlock 的 MONITOR 监控代码只提了名字。可带走的: ① safety/ liveness 二分;② mutex 与二元信号量的区别(解锁权);③ 四种 mutex 类型;④ wait 三铁律(while/信号不记账/一 cond 一 mutex); ⑤ barrier 的 SERIAL_THREAD 返回值;⑥ rwlock 读共享写独占与 读者/写者优先的取舍;⑦ rwlock 四条暗礁(递归配对/升级死锁/ 持写锁死亡/持读锁自动释放);⑧ 观察并发行为的方法:barrier 拉平起跑 + 日志锁序 + grep 时间戳。
7. 原文地图¶
| 主题 | 原书章 | 原文位置 |
|---|---|---|
| safety/liveness | Chapter 16: Thread Synchronization | text/22-ch16-chapter-16-thread-synchronization.txt:37(搜「safety property」) |
| mutex 语义五条 | 同上 | :112(搜「pthread_mutex_lock」) · :128(搜「deadlock」) |
| 与二元信号量的区别 | 同上 | :77(搜「binary semaphore」) |
| 初始化/destroy | 同上 | :88(搜「PTHREAD_MUTEX_INITIALIZER」) |
| 锁要短 | 同上 | :423(搜「as short as possible」) |
| 四种类型/recursive | 同上 | :428(搜「PTHREAD_MUTEX_NORMAL」) · :459(搜「RECURSIVE」) |
| threaded_progbar | 同上 | :258(搜「pthread_mutex_lock」) · :281(搜「random()」) · :305(搜「sigwait」) |
| 空转轮询问题 | 同上 | :590(搜「count < BUF_SIZE」) · :617(搜「block itself」) |
| cond wait 原子语义 | 同上 | :674(搜「pthread_cond_wait」) · :681(搜「must have locked」) |
| 虚假唤醒/while 铁律 | 同上 | :729(搜「spurious wake-ups」) · :741(搜「while ( count == BUF_SIZE」) |
| 信号不记账 | 同上 | :762(搜「not saved」) |
| barrier API | 同上 | :1144(搜「pthread_barrier_init」) · :1157(搜「pthread_barrier_wait」) · :1162(搜「SERIAL_THREAD」) |
| 求和策略权衡 | 同上 | :1216(搜「poor idea」) |
| 读写锁 | 同上 | :1590(搜「read-write lock」) · :1631(搜「pthread_rwlock_rdlock」) |
| rwlock 暗礁/Further Details | 同上 | :1746(搜「Further Details」) · :1749(搜「already holds a read lock」) · :1776(搜「EDEADLK」) |
| pthread_rwlock_demo | 同上 | :1860(搜「horse race」) · :1875(搜「rwlockdemo.log」) · :2144(搜「PREFER_WRITER_NONRECURSIVE_NP」) · :2169(搜「scarlet oak」) |
-
出处:「Chapter 16: Thread Synchronization」第 25–48 段(
text/22-ch16-chapter-16-thread-synchronization.txt:37,搜「safety property」)。 ↩↩ -
出处:「Chapter 16: Thread Synchronization」第 81–107 段(
text/22-ch16-chapter-16-thread-synchronization.txt:88,搜「PTHREAD_MUTEX_INITIALIZER」)。 ↩ -
出处:「Chapter 16: Thread Synchronization」第 109–157 段(
text/22-ch16-chapter-16-thread-synchronization.txt:112,搜「pthread_mutex_lock」)。 ↩ -
出处:「Chapter 16: Thread Synchronization」第 116–143 段(
text/22-ch16-chapter-16-thread-synchronization.txt:128,搜「deadlock」)。 ↩ -
出处:「Chapter 16: Thread Synchronization」第 77–79 段(
text/22-ch16-chapter-16-thread-synchronization.txt:77,搜「binary semaphore」)。 ↩ -
出处:「Chapter 16: Thread Synchronization」第 414–424 段(
text/22-ch16-chapter-16-thread-synchronization.txt:423,搜「as short as possible」)。 ↩↩ -
出处:「Chapter 16: Thread Synchronization」第 425–530 段(
text/22-ch16-chapter-16-thread-synchronization.txt:428,搜「PTHREAD_MUTEX_NORMAL」) · :459(搜「RECURSIVE」)。 ↩ -
出处:「Chapter 16: Thread Synchronization」第 171–412 段(
text/22-ch16-chapter-16-thread-synchronization.txt:258,搜「pthread_mutex_lock」) · :281(搜「random()」)。 ↩ -
出处:「Chapter 16: Thread Synchronization」第 296–326 段(
text/22-ch16-chapter-16-thread-synchronization.txt:305,搜「sigwait」)。 ↩↩ -
出处:「Chapter 16: Thread Synchronization」第 563–618 段(
text/22-ch16-chapter-16-thread-synchronization.txt:590,搜「count < BUF_SIZE」) · :617(搜「block itself」)。 ↩ -
出处:「Chapter 16: Thread Synchronization」第 532–561 段(
text/22-ch16-chapter-16-thread-synchronization.txt:537,搜「Boolean condition」)。 ↩ -
出处:「Chapter 16: Thread Synchronization」第 674–711 段(
text/22-ch16-chapter-16-thread-synchronization.txt:681,搜「must have locked」) · :717(搜「pthread_cond_broadcast」)。 ↩ -
出处:「Chapter 16: Thread Synchronization」第 727–752 段(
text/22-ch16-chapter-16-thread-synchronization.txt:729,搜「spurious wake-ups」) · :741(搜「while ( count == BUF_SIZE」)。 ↩ -
出处:「Chapter 16: Thread Synchronization」第 758–772 段(
text/22-ch16-chapter-16-thread-synchronization.txt:762,搜「not saved」)。 ↩ -
出处:「Chapter 16: Thread Synchronization」第 1135–1186 段(
text/22-ch16-chapter-16-thread-synchronization.txt:1144,搜「pthread_barrier_init」) · :1162(搜「SERIAL_THREAD」)。 ↩ -
出处:「Chapter 16: Thread Synchronization」第 1207–1235 段(
text/22-ch16-chapter-16-thread-synchronization.txt:1216,搜「poor idea」)。 ↩ -
出处:「Chapter 16: Thread Synchronization」第 1565–1607 段(
text/22-ch16-chapter-16-thread-synchronization.txt:1590,搜「read-write lock」)。 ↩ -
出处:「Chapter 16: Thread Synchronization」第 1608–1655 段(
text/22-ch16-chapter-16-thread-synchronization.txt:1631,搜「pthread_rwlock_rdlock」)。 ↩ -
出处:「Chapter 16: Thread Synchronization」第 1671–1683 段(
text/22-ch16-chapter-16-thread-synchronization.txt:1678,搜「guard」)。 ↩ -
出处:「Chapter 16: Thread Synchronization」第 1746–1787 段(
text/22-ch16-chapter-16-thread-synchronization.txt:1746,搜「Further Details」) · :1749(搜「already holds a read lock」) · :1769(搜「terminates while holding a write lock」) · :1776(搜「EDEADLK」) · :1786(搜「complex semantics」)。 ↩↩↩ -
出处:「Chapter 16: Thread Synchronization」第 1858–1935 段(
text/22-ch16-chapter-16-thread-synchronization.txt:1860,搜「horse race」) · :1870(搜「command line options」) · :1875(搜「rwlockdemo.log」) · :1878(搜「negates it」)。 ↩↩↩ -
出处:「Chapter 16: Thread Synchronization」第 1979–2205 段(
text/22-ch16-chapter-16-thread-synchronization.txt:1979,搜「writer start function」) · :2042(搜「getline(&buffer」) · :2144(搜「PREFER_WRITER_NONRECURSIVE_NP」) · :2151(搜「pthread_rwlock_demo [-r」) · :2169(搜「scarlet oak」) · :2192(搜「Reader 4 got the read lock」)。 ↩↩↩