1. 为什么我们需要线程互斥与同步?
当我在第一次接触多线程编程时,曾经天真地认为只要创建多个线程就能自动提升程序性能。直到有一天,我遇到了一个诡异的bug:银行账户余额在多个线程同时存取款时竟然出现了负数!这个经历让我深刻理解了线程安全的重要性。
现代操作系统中的线程就像厨房里的多位厨师。他们可以同时工作(并发执行),但如果都去抢同一把菜刀(共享资源),就可能发生危险。线程互斥就是给这把菜刀加锁的机制,确保同一时间只有一位厨师能使用它。
注意:在没有互斥保护的情况下,即使是简单的i++操作也可能出现问题,因为它实际上包含读取、修改、写入三个步骤,线程切换可能导致数据竞争。
1.1 线程互斥的本质
互斥(Mutex)解决的是"同一时刻只允许一个线程访问共享资源"的问题。想象医院就诊叫号系统:每个患者(线程)必须拿到号牌(锁)才能进入诊室(临界区),就诊结束后归还号牌。
在Linux系统中,最基础的互斥实现是pthread_mutex_t:
c复制pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void* thread_func(void* arg) {
pthread_mutex_lock(&mutex);
// 临界区代码
pthread_mutex_unlock(&mutex);
return NULL;
}
1.2 线程同步的艺术
同步则是解决线程间执行顺序的协调问题。就像接力赛中,第二棒选手必须等待第一棒选手交棒后才能起跑。常见的同步机制包括:
- 条件变量(Condition Variables):允许线程在某个条件不满足时主动等待
- 信号量(Semaphores):控制同时访问资源的线程数量
- 屏障(Barriers):确保多个线程都到达某一点后才继续执行
c复制// 典型的生产者-消费者模式中的条件变量使用
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void consumer() {
pthread_mutex_lock(&mutex);
while (buffer_empty()) {
pthread_cond_wait(&cond, &mutex); // 自动释放锁并等待
}
consume_item();
pthread_mutex_unlock(&mutex);
}
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 生产消费模型:经典中的经典
生产消费模型是我在面试候选人时必问的问题,因为它完美展现了线程同步与互斥的配合。去年我们团队就遇到过一个真实案例:日志服务在高并发下丢失了大量数据,最终就是通过优化生产消费队列解决的。
2.1 模型的核心组件
一个健壮的生产消费模型通常包含:
- 共享缓冲区(可以是数组、链表或队列)
- 生产者线程:创建数据并放入缓冲区
- 消费者线程:从缓冲区取出并处理数据
- 互斥锁:保护缓冲区的访问
- 条件变量:通知缓冲区状态变化
c复制#define BUFFER_SIZE 10
int buffer[BUFFER_SIZE];
int count = 0, in = 0, out = 0;
void producer() {
while (true) {
item = produce_item();
pthread_mutex_lock(&mutex);
while (count == BUFFER_SIZE) { // 缓冲区满则等待
pthread_cond_wait(&empty, &mutex);
}
buffer[in] = item;
in = (in + 1) % BUFFER_SIZE;
count++;
pthread_cond_signal(&full); // 通知消费者
pthread_mutex_unlock(&mutex);
}
}
2.2 三种常见的变体模型
在实际项目中,我根据不同的需求场景会选用不同变体:
- 单生产者单消费者(SPSC):最简单的模型,甚至可以不用锁(通过环形缓冲区和内存屏障实现)
- 多生产者单消费者(MPSC):需要严格保护生产者的写入操作
- 多生产者多消费者(MPMC):最复杂的场景,通常需要结合多
