第 4 章 · 进程同步

多线程并发访问共享数据产生竞态(结果取决于执行顺序);同步就是约束并发。

4.1 临界区

临界区:访问共享资源的代码段,须满足:

  1. 互斥:任一时刻至多一个进程在临界区。
  2. 前进:临界区空闲时,想进去的进程能尽快进。
  3. 有限等待:请求者等待时间有上界(不饿死)。

实现演进:软件法(Peterson,现代 CPU 乱序下已不可靠)→ 硬件法(关中断、TestAndSet/Swap 原子指令)→ 信号量/锁(封装硬件原语)。

4.2 信号量 PV 操作

信号量 = 整型变量 + 原子操作:

  • P(wait)s--,若结果 < 0 则阻塞自己。
  • V(signal)s++,若结果 ≤ 0 则唤醒一个等待者。
sem_t mutex = 1;          // 互斥信号量,初值 1
P(&mutex);                // 进入临界区前
/* 临界区 */
V(&mutex);                // 离开临界区后

初值 1 = 互斥锁;初值 N(>1)= 计数/资源管理(N 个空缓冲区)。

4.3 经典同步问题

  • 生产者-消费者:满则生产者停,空则消费者停。用 empty(初值 N)+ full(初值 0)+ 互斥 mutex。P 顺序关键:先 P(empty) 再 P(mutex),否则死锁。
  • 读者-写者:多读可并发,写者独占;读者优先/写者优先/公平三策略,读者优先可能饿死写者。
  • 哲学家就餐:5 哲学家,每人左右各一支筷,需同时拿两支才能吃。直接「先左后右」会死锁;解法:奇数先左偶数先右,或限制最多 4 人同时拿筷。

生产者-消费者典型实现(注意 P 顺序):

// 生产者:先 P(empty) 再 P(mutex),顺序不能反
P(&empty); P(&mutex);
put_item();
V(&mutex); V(&full);

// 消费者:先 P(full) 再 P(mutex)
P(&full); P(&mutex);
get_item();
V(&mutex); V(&empty);

4.4 互斥锁 vs 信号量

互斥锁(mutex)信号量(semaphore)
本质二元信号量计数信号量
用途仅互斥互斥 + 资源计数 + 同步顺序
所有权谁加锁谁解锁(有 owner)无所有权,任意线程可 V
递归不可递归加锁可多次 P
优先级有优先级继承防优先级反转通常无

忙等待(自旋锁)P 失败后循环 while (flag); 不放弃 CPU;阻塞则挂起让出 CPU。自旋锁不切上下文,适合临界区极短;阻塞适合临界区长(I/O),代价是一次上下文切换。