163 / 163 · C11 · 约 8 分钟
管程
先记住这句话
本附录介绍管程如何将共享数据与访问操作封装为单一模块并自动提供互斥,同时通过条件变量实现等待与唤醒,并对比Hoare与Mesa两种语义在实际系统中的差异。
管程如何自动保证互斥
管程把共享变量和操作它们的方法放在同一个模块里。任何线程进入方法时都会隐式获得模块锁,退出时释放,因此同一时刻最多只有一个线程在模块内部运行,从根本上消除了对共享数据的并发修改。
条件变量必须与显式状态配合
仅有锁不够,线程还需要在缓冲区满或空等条件不满足时睡眠。管程为此提供条件变量,但条件变量本身不保存计数值,必须额外维护一个状态整数,线程先检查该整数再决定是否调用wait。
Hoare语义立即切换而Mesa语义只给提示
Hoare语义下signal会立刻把锁和控制权交给一个等待者,信号者自己暂停。Mesa语义把signal当作提示,信号者继续执行直到退出模块,等待者稍后才被调度,因此等待者醒来后必须重新测试条件。
从理论到语言实现的演变
最初的理论设计追求证明方便,后来的系统语言为了实现简单改用Mesa风格。今日Java等语言用synchronized方法提供近似管程的效果,程序员仍需自己用wait/notify并配合循环检查。
常见误区
- 在Mesa语义下用if而非while判断条件,导致线程在条件已被其他线程破坏后继续执行。
- 把条件变量当成信号量使用,忘记同时维护独立的状态计数,造成永远等待或错误唤醒。
运行一个例子
最低标准 C11 · 完整程序 · 下载 .c
#include <stdio.h>
#include <pthread.h>
int balance = 0;
pthread_mutex_t mon = PTHREAD_MUTEX_INITIALIZER;
void deposit(int n) {
pthread_mutex_lock(&mon);
balance += n;
pthread_mutex_unlock(&mon);
}
void *worker(void *arg) { (void)arg;
for (int i = 0; i < 1000; i++)
deposit(1);
return NULL;
}
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, worker, NULL);
pthread_create(&t2, NULL, worker, NULL);
pthread_join(t1, NULL);
pthread_join(t2, NULL);
printf("%d\n", balance);
return 0;
}
在本地编译
gcc -std=c11 -Wall -Wextra -Wpedantic -Werror -pthread ostep-monitors.c -o example && ./example预期结果
2000
CHECK YOUR UNDERSTANDING
合上答案,试着解释。
为什么采用Mesa语义的有界缓冲区管程必须把“缓冲区是否为空”的判断写成while循环?
查看参考答案
因为signal只是把等待线程移到就绪队列,并不保证它立刻运行;在它真正获得锁之前,另一个消费者可能已经把刚放入的数据取走,因此必须再次检查状态。
继续查证
标准草案与官方章节会更新;版本标记只说明示例最低要求。