上一期讲了 Linux 多线程编程基础。
这一期讲互斥锁和条件变量。
一、线程安全问题
当多个线程同时读写同一个共享变量时,因为 CPU 调度的不确定性,可能会出现数据不一致的情况,这就是线程安全问题。用一个经典的例子来演示:两个线程同时对一个全局变量做 100 万次自增操作,看看最终结果是不是 200 万。运行这个程序,会发现最终结果几乎永远小于 200 万,这就是数据竞争导致的。因为g_count++不是原子操作,它会被拆分为「读取变量→加 1→写回内存」三步,两个线程可能在中间步骤被切换,导致数据丢失。解决这个问题的核心思路是:保证同一时间只有一个线程访问共享资源,这就是线程同步。二、互斥锁(Mutex)
互斥锁是最常用的线程同步机制。它的原理很简单:访问共享资源前先加锁,访问完后解锁。同一时间只有一个线程能持有锁,其他线程加锁时会阻塞,直到锁被释放。1. 互斥锁相关函数
pthread_mutex_init:初始化互斥锁
mutex:互斥锁变量的指针
attr:锁属性,通常为 NULL,使用默认属性
返回值:成功返回 0,失败返回错误码
也可以用宏静态初始化:pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;pthread_mutex_lock:加锁
如果锁已经被其他线程持有,调用这个函数会阻塞,直到锁被释放。pthread_mutex_unlock:解锁
pthread_mutex_destroy:销毁互斥锁
2. 用互斥锁解决自增问题
3. 互斥锁的注意事项
加锁粒度要尽可能小:只在访问共享资源的代码段加锁,不要把整个函数都加锁,否则会降低并发性。
避免死锁:如果一个线程持有锁 A,又去申请锁 B;另一个线程持有锁 B,又去申请锁 A,两个线程就会永远阻塞。
谁加锁谁解锁:加锁和解锁必须在同一个线程中执行。
不要重复加锁:默认的互斥锁不支持递归加锁,重复加锁会导致死锁。
三、条件变量(Condition Variable)
互斥锁解决了资源互斥访问的问题,但有时候我们还需要一种机制:让线程在某个条件不满足时阻塞等待,当条件满足时被其他线程唤醒。这就是条件变量的作用。条件变量必须和互斥锁配合使用,因为条件的判断本身也是共享资源,需要互斥锁保护。1. 条件变量相关函数
pthread_cond_init:初始化条件变量
静态初始化:pthread_cond_t cond = PTHREAD_COND_INITIALIZER;pthread_cond_wait:等待条件
释放持有的互斥锁
阻塞等待,直到被唤醒
被唤醒后重新获取互斥锁
pthread_cond_signal:唤醒一个等待的线程
pthread_cond_broadcast:唤醒所有等待的线程
pthread_cond_destroy:销毁条件变量
2. 实战:生产者消费者模型
生产者消费者模型是条件变量最经典的应用场景:生产者线程生产数据放到队列中,消费者线程从队列中取出数据消费。队列为空时消费者等待,队列满时生产者等待。注意:判断条件时必须用while而不是if,因为线程被唤醒后,条件可能又不满足了(虚假唤醒),需要重新判断。四、死锁的产生与避免
死锁是多线程编程中最常见的问题,当两个或多个线程互相等待对方持有的锁时,就会永远阻塞。死锁产生的四个必要条件
互斥条件:资源同一时间只能被一个线程持有
持有并等待:线程已经持有了一些资源,又去申请其他线程持有的资源
不可剥夺:资源只能由持有者主动释放,不能被其他线程强行剥夺
循环等待:线程之间形成循环等待链
避免死锁的方法
破坏循环等待:所有线程按相同的顺序申请锁
破坏持有并等待:一次性申请所有需要的锁
设置超时时间:加锁时设置超时,避免无限等待
避免嵌套加锁:尽量不要在持有锁的时候再申请其他锁
五、总结
线程安全问题:多个线程同时访问共享资源导致的数据不一致
互斥锁:保证同一时间只有一个线程访问共享资源,解决数据竞争
条件变量:实现线程之间的等待与唤醒,经典应用是生产者消费者模型
死锁:产生的四个必要条件与避免方法
下一期讲更多线程同步工具:读写锁、信号量和自旋锁,以及它们的适用场景。大家有任何不懂的地方,或者敲代码遇到了 bug,都可以在评论区留言,我都会一一回复。如果这篇内容对你有帮助,别忘了点赞、在看、转发给身边同样在学 C 语言的朋友。