4. 同步与互斥:互斥锁、条件变量、信号量、读写锁、死锁预防
多核编程里,最让人头疼的,就是同步与互斥。
说白了,就是多个线程同时访问同一块数据,怎么保证不出乱子。我早年做QNX项目时,就吃过这个亏。一个全局变量,两个核同时写,结果数据全乱了。从那以后,我对同步机制就特别上心。
4.1 互斥锁(Mutex)
互斥锁是最基础的同步工具。它的作用很简单:保护临界区,同一时间只允许一个线程进入。
在QNX里,互斥锁用 pthread_mutex_t 类型表示。我习惯用静态初始化,省事又安全:
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void worker(void *arg) {
pthread_mutex_lock(&mutex);
// 临界区代码
// 比如修改共享变量
pthread_mutex_unlock(&mutex);
}
嗯,这里要注意:锁的粒度要适中。锁得太细,性能上不去;锁得太粗,并发度又没了。我在项目中遇到过,有人把整个循环都锁住,结果多核跑成了单核,性能反而下降了。
4.2 条件变量(Condition Variable)
条件变量解决的是「等待某个条件成立」的问题。比如生产者-消费者模型,消费者要等生产者生产出数据才能消费。
条件变量必须和互斥锁配合使用。为什么?因为条件判断本身也需要保护。我见过有人把条件变量当信号量用,结果出了大问题。
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int data_ready = 0;
// 生产者
void producer() {
pthread_mutex_lock(&mutex);
data_ready = 1;
pthread_cond_signal(&cond); // 唤醒等待的消费者
pthread_mutex_unlock(&mutex);
}
// 消费者
void consumer() {
pthread_mutex_lock(&mutex);
while (!data_ready) {
pthread_cond_wait(&cond, &mutex); // 等待条件满足
}
// 消费数据
pthread_mutex_unlock(&mutex);
}
注意那个 while 循环,不是 if。为什么?因为 pthread_cond_wait 可能被虚假唤醒。你想想看,如果只用 if,被唤醒后条件可能还没满足,那就出事了。
4.3 信号量(Semaphore)
信号量可以看作是一个计数器。它不一定要和锁绑定,使用起来更灵活。
QNX 里用 sem_t 类型。我常用它来做资源池管理,比如控制同时访问某个设备的线程数量:
sem_t sem;
sem_init(&sem, 0, 3); // 最多允许3个线程同时访问
void access_device() {
sem_wait(&sem); // 获取信号量
// 访问设备
sem_post(&sem); // 释放信号量
}
信号量和互斥锁的区别在哪?互斥锁是「谁持有谁释放」,信号量可以由不同线程释放。这个特性在某些场景下特别有用。
我曾经用信号量实现过一个任务分发系统。主线程发信号,工作线程等信号。用互斥锁反而别扭,信号量就刚刚好。
4.4 读写锁(Read-Write Lock)
读写锁是个好东西。它允许多个线程同时读,但写的时候只能有一个线程,而且不能有读操作。
适合「读多写少」的场景。比如配置表、路由表这些,大部分时间都在查,很少更新。
pthread_rwlock_t rwlock = PTHREAD_RWLOCK_INITIALIZER;
void read_data() {
pthread_rwlock_rdlock(&rwlock);
// 读取数据
pthread_rwlock_unlock(&rwlock);
}
void write_data() {
pthread_rwlock_wrlock(&rwlock);
// 写入数据
pthread_rwlock_unlock(&rwlock);
}
但要注意,读写锁不是万能的。如果写操作很频繁,读操作反而会被频繁阻塞,性能还不如互斥锁。我建议你先评估一下读写比例,再决定用哪种锁。
4.5 死锁预防
死锁是同步编程里最可怕的敌人。四个必要条件:互斥、持有并等待、不可剥夺、循环等待。只要打破其中一个,死锁就不会发生。
我常用的预防策略有几种:
- 锁顺序一致: 所有线程按同样的顺序获取锁。比如先锁A再锁B,不要有的线程先锁B再锁A。
- 使用超时锁:
pthread_mutex_timedlock可以设置超时时间,避免无限等待。 - 减少锁的持有时间: 临界区越小,死锁概率越低。
- 避免嵌套锁: 能用一个锁解决的问题,别用两个。
我曾经遇到过一个死锁问题,查了两天才找到原因。两个线程,一个锁A再锁B,另一个锁B再锁A。平时没事,但某个特定时序下就卡死了。从那以后,我强制团队所有成员必须按固定顺序获取锁。
pthread_mutexattr_setprotocol 设置了优先级继承协议,才彻底解决。死锁不一定都是代码逻辑问题,调度策略也可能引发。
4.6 如何选择同步机制
说了这么多,到底该用哪个?我总结了一张表,方便你对照:
| 场景 | 推荐机制 | 原因 |
|---|---|---|
| 保护临界区 | 互斥锁 | 简单直接,性能好 |
| 等待条件满足 | 条件变量 | 避免忙等待,节省CPU |
| 资源计数控制 | 信号量 | 灵活,可跨线程释放 |
| 读多写少 | 读写锁 | 提高读并发性能 |
| 避免死锁 | 超时锁 + 固定顺序 | 打破死锁条件 |
嗯,最后说一句。同步机制没有银弹。你得多试、多测,才能找到最适合自己项目的方案。我做了这么多年嵌入式,每次选型还是会仔细掂量一下。
记住一点:简单就是美。能用互斥锁解决的问题,别整花里胡哨的。系统越复杂,越容易出问题。