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,被唤醒后条件可能还没满足,那就出事了。

警告: 条件变量的等待一定要用 while 循环检查条件,不要用 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);
}

但要注意,读写锁不是万能的。如果写操作很频繁,读操作反而会被频繁阻塞,性能还不如互斥锁。我建议你先评估一下读写比例,再决定用哪种锁。

核心要点: 读写锁适合读多写少的场景。如果写操作占比超过20%,直接用互斥锁可能更好。

4.5 死锁预防

死锁是同步编程里最可怕的敌人。四个必要条件:互斥、持有并等待、不可剥夺、循环等待。只要打破其中一个,死锁就不会发生。

我常用的预防策略有几种:

  • 锁顺序一致: 所有线程按同样的顺序获取锁。比如先锁A再锁B,不要有的线程先锁B再锁A。
  • 使用超时锁: pthread_mutex_timedlock 可以设置超时时间,避免无限等待。
  • 减少锁的持有时间: 临界区越小,死锁概率越低。
  • 避免嵌套锁: 能用一个锁解决的问题,别用两个。

我曾经遇到过一个死锁问题,查了两天才找到原因。两个线程,一个锁A再锁B,另一个锁B再锁A。平时没事,但某个特定时序下就卡死了。从那以后,我强制团队所有成员必须按固定顺序获取锁。

避坑指南: 我曾经在调试一个多核系统时,发现死锁只在特定负载下出现。后来用 QNX 的 pthread_mutexattr_setprotocol 设置了优先级继承协议,才彻底解决。死锁不一定都是代码逻辑问题,调度策略也可能引发。

4.6 如何选择同步机制

说了这么多,到底该用哪个?我总结了一张表,方便你对照:

场景 推荐机制 原因
保护临界区 互斥锁 简单直接,性能好
等待条件满足 条件变量 避免忙等待,节省CPU
资源计数控制 信号量 灵活,可跨线程释放
读多写少 读写锁 提高读并发性能
避免死锁 超时锁 + 固定顺序 打破死锁条件

嗯,最后说一句。同步机制没有银弹。你得多试、多测,才能找到最适合自己项目的方案。我做了这么多年嵌入式,每次选型还是会仔细掂量一下。

记住一点:简单就是美。能用互斥锁解决的问题,别整花里胡哨的。系统越复杂,越容易出问题。