当前位置:首页>Linux>Linux线程同步:互斥锁与条件变量示例

Linux线程同步:互斥锁与条件变量示例

  • 2026-10-11 06:18:12
Linux线程同步:互斥锁与条件变量示例

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(&lock)
加锁
靠变量名 lock 猜
sem_post(&lock)
解锁
靠变量名 lock 猜
sem_wait(&count)
等待事件
靠变量名 count 猜
sem_post(&count)
触发事件
靠变量名 count 猜

同一个 sem_wait,有时是 "抢锁",有时是 "等数据"。同一个 sem_post,有时是 "解锁",有时是 "发通知"。

开发者每次看到 sem_wait,都必须先看参数变量名,才能确定这行代码在干什么。这种 "语义映射" 的工序,在代码审查和长期维护中累积的认知成本非常高。

而 Mutex+Cond 版本:

函数
唯一语义
pthread_mutex_lock
加锁
pthread_mutex_unlock
解锁
pthread_cond_wait
等待条件
pthread_cond_signal
通知条件变化

每个函数只有一种语义,看函数名就知道在干什么,不需要靠变量名去猜。

六、总结

Mutex+Cond 的优势不在于 "功能更强",而在于 "语义更清晰"。 在大型工程中,代码是写给维护者看的,不是写给编译器看的。一个能减少心智负担的设计,比一个 "看起来更简洁" 的设计,长期价值更高。

往期推荐:

Linux同步: semaphore — 一个不待见的同步机制

最新文章

随机文章