Linux线程同步:互斥锁与条件变量示例
一、前言
上一篇文章我们聊过,信号量由于语义不清、职责混乱,内核里推荐使用 completion 和 mutex 来替代。用户态也存在同样的问题——pthread 中推荐使用互斥锁 + 条件变量:
pthread_mutex_lock/unlock:唯一语义——加锁/解锁
pthread_cond_wait/signal:唯一语义——等待条件/通知条件变化
每个函数只做一件事,看函数名就知道它在干什么。
本文给个"生产者-消费者"的完整示例。
二、场景与设计
场景:一个生产者线程生产数字,一个消费者线程消费数字,通过队列传递数据。
struct Queue { Node* head; // 队头(消费端) Node* tail; // 队尾(生产端) int size; // 当前队列长度 pthread_mutex_t mutex; // 保护整个队列 pthread_cond_t cond; // 条件变量:队列非空};
三、完整代码示例
生产者调用queue_push把数据入列并通知消费者,消费者调用queue_pop等待数据到来。
#include<stdio.h>#include<stdlib.h>#include<pthread.h>#include<unistd.h>// ============ 队列结构 ============typedef struct Node { int data; struct Node* next;} Node;typedef struct { Node* head; Node* tail; int size; pthread_mutex_t mutex; pthread_cond_t cond; // 条件:队列非空} Queue;// ============ 队列操作 ============voidqueue_init(Queue* q){ q->head = q->tail = NULL; q->size = 0; pthread_mutex_init(&q->mutex, NULL); pthread_cond_init(&q->cond, NULL);}voidqueue_push(Queue* q, int data){ Node* node = malloc(sizeof(Node)); node->data = data; node->next = NULL; pthread_mutex_lock(&q->mutex); if (q->tail) { q->tail->next = node; q->tail = node; } else { q->head = q->tail = node; } q->size++; pthread_cond_signal(&q->cond); // 通知消费者:队列非空 pthread_mutex_unlock(&q->mutex);}intqueue_pop(Queue* q){ pthread_mutex_lock(&q->mutex); // 用 while 而非 if:防止虚假唤醒 while (q->size == 0) { pthread_cond_wait(&q->cond, &q->mutex); } Node* node = q->head; int data = node->data; q->head = node->next; if (!q->head) { q->tail = NULL; } q->size--; free(node); pthread_mutex_unlock(&q->mutex); return data;}// ============ 生产者线程 ============Queue g_queue;void* producer(void* arg){ int value = 0; while (1) { queue_push(&g_queue, ++value); printf("[生产者] 生产: %d, 队列长度: %d\n", value, g_queue.size); sleep(1); } return NULL;}// ============ 消费者线程 ============void* consumer(void* arg){ while (1) { int data = queue_pop(&g_queue); printf("[消费者] 消费: %d, 队列剩余: %d\n", data, g_queue.size); } return NULL;}// ============ 主函数 ============intmain(){ queue_init(&g_queue); pthread_t p1, c1; pthread_create(&p1, NULL, producer, NULL); pthread_create(&c1, NULL, consumer, NULL); pthread_join(p1, NULL); pthread_join(c1, NULL); return 0;}
编译运行
gcc -o demo demo.c -pthread./demo
预期输出
[生产者] 生产: 1, 队列长度: 1[消费者] 消费: 1, 队列剩余: 0[生产者] 生产: 2, 队列长度: 1[消费者] 消费: 2, 队列剩余: 0[生产者] 生产: 3, 队列长度: 1...
四、核心规则
规则 1:条件判断必须用 while,禁止用 if
// ❌ 错误if (q->size == 0) { pthread_cond_wait(&q->cond, &q->mutex);}// ✅ 正确while (q->size == 0) { pthread_cond_wait(&q->cond, &q->mutex);}
原因:pthread_cond_wait 返回后,条件不一定成立。可能是信号导致提前唤醒(虚假唤醒),也可能是唤醒后资源被其他线程抢先消耗了。用 while 确保每次醒后重新检查条件,不满足就继续等。
规则 2:pthread_cond_wait () 必须持有互斥锁
// ✅ 正确pthread_mutex_lock(&q->mutex);while (q->size == 0) { pthread_cond_wait(&q->cond, &q->mutex);}pthread_mutex_unlock(&q->mutex);
pthread_cond_wait内部会原子地完成两步:释放锁 + 阻塞等待。如果调用前没有持锁,这个 "原子性" 就无从谈起,可能导致信号丢失。
规则 3:先改数据,再 signal
// ✅ 正确q->tail->next = node; // 先改数据pthread_cond_signal(&q->cond); // 再通知// ❌ 错误pthread_cond_signal(&q->cond); // 先通知q->tail->next = node; // 消费者醒来读到的是旧数据
原则:signal 的作用是 "条件已变化,快来检查",而不是 "数据已就绪,直接来取"。消费者被唤醒后还要通过 while 重新检查条件,所以必须先让数据到位。
规则 4:必须配对初始化与销毁
// 动态初始化pthread_mutex_init(&mutex, NULL);pthread_cond_init(&cond, NULL);// ... 使用 ...pthread_mutex_destroy(&mutex);pthread_cond_destroy(&cond);
静态初始化(全局或静态变量):
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
五、为什么不用信号量?
用信号量实现同样的队列同步,需要两个信号量:
sem_t lock; // 充当互斥锁(初始值1)sem_t count; // 队列元素计数(初始值0)voidpush(Queue* q, int data) { sem_wait(&lock); // ← 这是"加锁" // 操作链表... sem_post(&lock); // ← 这是"解锁" sem_post(&count); // ← 这是"触发事件"}intpop(Queue* q) { sem_wait(&count); // ← 这是"等待事件" sem_wait(&lock); // ← 这是"加锁" // 操作链表... sem_post(&lock); // ← 这是"解锁" return data;}
问题出在哪?
同一个 sem_wait,有时是 "抢锁",有时是 "等数据"。同一个 sem_post,有时是 "解锁",有时是 "发通知"。
开发者每次看到 sem_wait,都必须先看参数变量名,才能确定这行代码在干什么。这种 "语义映射" 的工序,在代码审查和长期维护中累积的认知成本非常高。
而 Mutex+Cond 版本:
每个函数只有一种语义,看函数名就知道在干什么,不需要靠变量名去猜。
六、总结
Mutex+Cond 的优势不在于 "功能更强",而在于 "语义更清晰"。 在大型工程中,代码是写给维护者看的,不是写给编译器看的。一个能减少心智负担的设计,比一个 "看起来更简洁" 的设计,长期价值更高。
往期推荐:
Linux同步: semaphore — 一个不待见的同步机制