操作系统中的并发同步机制:条件变量、信号量与经典问题解析
并发环境下的线程同步
在多线程程序中,不同执行流对共享资源的访问必须协调一致,以避免数据竞争和状态不一致。同步的核心在于确保多个线程在特定条件下按预期顺序推进。 当多个线程共同操作全局变量时,若缺乏同步机制,最终结果可能不可预测。例如两个线程同时递增一个计数器,由于读取-修改-写入过程非原子性,可能导致丢失更新。因此需要使用互斥锁(mutex)来保护临界区,保证同一时间只有一个线程可以修改共享状态。生产者-消费者模型
该模型是并发编程中最基础的设计模式之一,广泛应用于任务队列、缓冲区管理等场景。生产者生成数据并放入队列,消费者从队列取出并处理数据。为防止越界访问,需满足以下约束:- 当缓冲区满时,生产者必须等待
- 当缓冲区空时,消费者必须等待
条件变量的正确用法
使用条件变量时应遵循标准范式:mutex_lock(&mtx);
while (!condition_met()) {
cond_wait(&cv, &mtx);
}
// 此处断言 condition 成立
do_work();
mutex_unlock(&mtx);
// 其他线程中
mutex_lock(&mtx);
update_state();
cond_broadcast(&cv); // 或 cond_signal
mutex_unlock(&mtx);
注意使用while而非if判断条件,以防虚假唤醒(spurious wakeup)。此外,在多生产者/多消费者场景下,应使用cond_broadcast通知所有等待线程,避免遗漏可运行的线程。
信号量机制
信号量是一种更高级的同步原语,由Dijkstra提出,适用于资源计数类问题。它维护一个整型值表示可用资源数量,提供两个原子操作:- P操作(wait/down):尝试获取一个单位资源,若无可用资源则阻塞
- V操作(signal/up):释放一个单位资源,唤醒等待者
sem_t empty_count; // 初始值为N,表示空槽位数
sem_t fill_count; // 初始值为0,表示已填充项数
void producer() {
while (1) {
produce_item();
P(&empty_count); // 等待空位
add_to_buffer();
V(&fill_count); // 增加已填项
}
}
void consumer() {
while (1) {
P(&fill_count); // 等待有数据
remove_from_buffer();
V(&empty_count); // 释放空位
consume_item();
}
}
这种方法天然支持多个生产者和消费者,且无需显式管理条件判断。
哲学家进餐问题
五个哲学家围坐圆桌,每人左右各有一把叉子。进餐需同时持有两把叉子,否则等待。这是典型的资源竞争问题,直接使用互斥锁容易导致死锁——所有哲学家同时拿起左手边的叉子,陷入永久等待。解决方案对比
方案一:限制同时尝试进餐的人数引入一个计数信号量,最多允许四位哲学家同时请求叉子,从而打破循环等待条件。
sem_t table; // 初始值为4
void philosopher(int id) {
int left = id;
int right = (id + 1) % 5;
while (1) {
think();
P(&table); // 获取入座许可
P(&forks[left]); // 拿起左叉
P(&forks[right]); // 拿起右叉
eat();
V(&forks[right]);
V(&forks[left]);
V(&table); // 离开座位
}
}
方案二:集中式调度(服务生模式)设置一个协调者(如服务员),统一管理所有叉子的分配。哲学家需向服务生申请用餐,获得授权后才能拿取双叉。这将分布式决策转为集中控制,从根本上避免死锁。
mutex_t server_lock;
void request_eat(int id) {
mutex_lock(&server_lock);
while (!can_grant_forks(id)) {
wait_for_permission();
}
grant_forks(id);
mutex_unlock(&server_lock);
}
void done_eat(int id) {
mutex_lock(&server_lock);
release_forks(id);
signal_all_waiting();
mutex_unlock(&server_lock);
}
此方法扩展性强,但中心节点可能成为性能瓶颈。在高并发系统中可通过分片或去中心化策略优化。
总结
同步机制的选择应基于具体需求:- 简单互斥访问 → 使用互斥锁
- 复杂条件依赖 → 条件变量 + 互斥锁
- 资源计数问题 → 信号量
- 避免死锁 → 破坏四个必要条件(互斥、占有等待、不可抢占、循环等待)