编程语言研究书库

线程同步 — 互斥锁、条件变量、屏障与读写锁

线程同步 — 互斥锁、条件变量、屏障与读写锁

这一章讲什么: 线程共享内存,爽;线程同时改内存,炸。 第 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。

三条铁律:

  1. wait 必须放在 while 里,不许用 if——存在虚假唤醒 (spurious wakeup):signal 可能意外唤醒多个;POSIX 明说 「修这个问题会降低并发度」所以不修。醒来 ≠ 条件为真, 必须重新检查13;
  2. 信号不记账:signal 先于 wait 到达 = 丢了,没人醒;
  3. 一个条件变量同时只绑定一个 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。

两个承重决策2122:

  1. 起跑用屏障拉平。 不按赛马式起跑设计的话,先创建的 线程总是先拿到锁——若恰好是写者,读者从开场就被饿死, 实验数据没意义。所以每个 reader/writer 的 start 函数 开头都先过一道 barrier。
  2. 防写者饿死靠锁属性,但有前提。 默认(不给 -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/livenessChapter 16: Thread Synchronizationtext/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」)

Footnotes

  1. 出处:「Chapter 16: Thread Synchronization」第 25–48 段(text/22-ch16-chapter-16-thread-synchronization.txt:37,搜「safety property」)。 ↩ ↩2

  2. 出处:「Chapter 16: Thread Synchronization」第 81–107 段(text/22-ch16-chapter-16-thread-synchronization.txt:88,搜「PTHREAD_MUTEX_INITIALIZER」)。 ↩

  3. 出处:「Chapter 16: Thread Synchronization」第 109–157 段(text/22-ch16-chapter-16-thread-synchronization.txt:112,搜「pthread_mutex_lock」)。 ↩

  4. 出处:「Chapter 16: Thread Synchronization」第 116–143 段(text/22-ch16-chapter-16-thread-synchronization.txt:128,搜「deadlock」)。 ↩

  5. 出处:「Chapter 16: Thread Synchronization」第 77–79 段(text/22-ch16-chapter-16-thread-synchronization.txt:77,搜「binary semaphore」)。 ↩

  6. 出处:「Chapter 16: Thread Synchronization」第 414–424 段(text/22-ch16-chapter-16-thread-synchronization.txt:423,搜「as short as possible」)。 ↩ ↩2

  7. 出处:「Chapter 16: Thread Synchronization」第 425–530 段(text/22-ch16-chapter-16-thread-synchronization.txt:428,搜「PTHREAD_MUTEX_NORMAL」) · :459(搜「RECURSIVE」)。 ↩

  8. 出处:「Chapter 16: Thread Synchronization」第 171–412 段(text/22-ch16-chapter-16-thread-synchronization.txt:258,搜「pthread_mutex_lock」) · :281(搜「random()」)。 ↩

  9. 出处:「Chapter 16: Thread Synchronization」第 296–326 段(text/22-ch16-chapter-16-thread-synchronization.txt:305,搜「sigwait」)。 ↩ ↩2

  10. 出处:「Chapter 16: Thread Synchronization」第 563–618 段(text/22-ch16-chapter-16-thread-synchronization.txt:590,搜「count < BUF_SIZE」) · :617(搜「block itself」)。 ↩

  11. 出处:「Chapter 16: Thread Synchronization」第 532–561 段(text/22-ch16-chapter-16-thread-synchronization.txt:537,搜「Boolean condition」)。 ↩

  12. 出处:「Chapter 16: Thread Synchronization」第 674–711 段(text/22-ch16-chapter-16-thread-synchronization.txt:681,搜「must have locked」) · :717(搜「pthread_cond_broadcast」)。 ↩

  13. 出处:「Chapter 16: Thread Synchronization」第 727–752 段(text/22-ch16-chapter-16-thread-synchronization.txt:729,搜「spurious wake-ups」) · :741(搜「while ( count == BUF_SIZE」)。 ↩

  14. 出处:「Chapter 16: Thread Synchronization」第 758–772 段(text/22-ch16-chapter-16-thread-synchronization.txt:762,搜「not saved」)。 ↩

  15. 出处:「Chapter 16: Thread Synchronization」第 1135–1186 段(text/22-ch16-chapter-16-thread-synchronization.txt:1144,搜「pthread_barrier_init」) · :1162(搜「SERIAL_THREAD」)。 ↩

  16. 出处:「Chapter 16: Thread Synchronization」第 1207–1235 段(text/22-ch16-chapter-16-thread-synchronization.txt:1216,搜「poor idea」)。 ↩

  17. 出处:「Chapter 16: Thread Synchronization」第 1565–1607 段(text/22-ch16-chapter-16-thread-synchronization.txt:1590,搜「read-write lock」)。 ↩

  18. 出处:「Chapter 16: Thread Synchronization」第 1608–1655 段(text/22-ch16-chapter-16-thread-synchronization.txt:1631,搜「pthread_rwlock_rdlock」)。 ↩

  19. 出处:「Chapter 16: Thread Synchronization」第 1671–1683 段(text/22-ch16-chapter-16-thread-synchronization.txt:1678,搜「guard」)。 ↩

  20. 出处:「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」)。 ↩ ↩2 ↩3

  21. 出处:「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」)。 ↩ ↩2 ↩3

  22. 出处:「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」)。 ↩ ↩2 ↩3

On this page