Day5 条件变量&互斥锁编程模板梳理

3 阅读4分钟

Day 5: 线程同步 - 互斥锁与条件变量

概述

本文整理互斥锁(Mutex)和条件变量(Condition Variable)的经典使用模式,代码改编自经典教材,适合学习理解原理。

⚠️ 注意:本文代码为教学示例,不建议直接用于生产环境。生产环境请使用经过工业验证的库。


1. 线程安全队列(最经典模式)

出处:《C++ Concurrency in Action》第2版,Anthony Williams 著,第6章

代码实现

#include <queue>
#include <mutex>
#include <condition_variable>

template<typename T>
class threadsafe_queue {
private:
    std::queue<T> data_queue;
    mutable std::mutex mut;
    std::condition_variable data_cond;

public:
    // 入队
    void push(T new_value) {
        std::lock_guard<std::mutex> lk(mut);
        data_queue.push(std::move(new_value));
        data_cond.notify_one();  // 通知一个等待的线程
    }

    // 出队(阻塞等待)
    void wait_and_pop(T& value) {
        std::unique_lock<std::mutex> lk(mut);
        // 条件变量等待:自动释放锁,被唤醒后自动加锁
        data_cond.wait(lk, [this] { return !data_queue.empty(); });
        value = std::move(data_queue.front());
        data_queue.pop();
    }

    // 出队(非阻塞)
    bool try_pop(T& value) {
        std::lock_guard<std::mutex> lk(mut);
        if (data_queue.empty()) {
            return false;
        }
        value = std::move(data_queue.front());
        data_queue.pop();
        return true;
    }

    // 判空
    bool empty() const {
        std::lock_guard<std::mutex> lk(mut);
        return data_queue.empty();
    }
};

工作流程图

sequenceDiagram
    participant Producer as 生产者线程
    participant Queue as threadsafe_queue
    participant Consumer as 消费者线程
    participant Mutex as mutex
    participant CV as condition_variable

    Note over Consumer: 队列为空,等待中
    Consumer->>CV: wait(lk, predicate)
    CV->>Mutex: 自动释放锁
    
    Producer->>Mutex: lock_guard 加锁
    Producer->>Queue: push(value)
    Producer->>CV: notify_one()
    Producer->>Mutex: 自动解锁
    
    CV->>Consumer: 被唤醒
    CV->>Mutex: 自动加锁
    Consumer->>Queue: 检查 predicate (非空)
    Consumer->>Queue: pop()
    Consumer->>Mutex: 自动解锁

核心技巧

技巧说明
std::lock_guard简单作用域锁,RAII 自动管理
std::unique_lock配合条件变量使用(需要解锁/加锁能力)
wait() 的谓词参数防止虚假唤醒(spurious wakeup)
notify_one()只唤醒一个等待线程,减少竞争

2. C 语言生产者-消费者(双条件变量模式)

出处:《UNIX环境高级编程》第3版,W. Richard Stevens 著,第11-12章

代码实现

#include <pthread.h>
#include <stdlib.h>

#define BUFFER_SIZE 10

typedef struct {
    int buffer[BUFFER_SIZE];
    int count;          // 当前元素数
    int in;             // 写入位置
    int out;            // 读取位置
    
    pthread_mutex_t mutex;
    pthread_cond_t not_full;   // 缓冲区不满
    pthread_cond_t not_empty;  // 缓冲区不空
} buffer_t;

// 初始化
void buffer_init(buffer_t *b) {
    b->count = 0;
    b->in = 0;
    b->out = 0;
    pthread_mutex_init(&b->mutex, NULL);
    pthread_cond_init(&b->not_full, NULL);
    pthread_cond_init(&b->not_empty, NULL);
}

// 生产者:放入数据
void buffer_put(buffer_t *b, int item) {
    pthread_mutex_lock(&b->mutex);
    
    // 等待缓冲区不满
    while (b->count == BUFFER_SIZE) {
        pthread_cond_wait(&b->not_full, &b->mutex);
    }
    
    b->buffer[b->in] = item;
    b->in = (b->in + 1) % BUFFER_SIZE;
    b->count++;
    
    // 通知消费者:缓冲区不空了
    pthread_cond_signal(&b->not_empty);
    
    pthread_mutex_unlock(&b->mutex);
}

// 消费者:取出数据
int buffer_get(buffer_t *b) {
    pthread_mutex_lock(&b->mutex);
    
    // 等待缓冲区不空
    while (b->count == 0) {
        pthread_cond_wait(&b->not_empty, &b->mutex);
    }
    
    int item = b->buffer[b->out];
    b->out = (b->out + 1) % BUFFER_SIZE;
    b->count--;
    
    // 通知生产者:缓冲区不满了
    pthread_cond_signal(&b->not_full);
    
    pthread_mutex_unlock(&b->mutex);
    
    return item;
}

状态转换图

stateDiagram-v2
    [*] --> Empty: 初始化
    Empty --> Partial: buffer_put (count=1)
    Partial --> Partial: buffer_put / buffer_get
    Partial --> Full: buffer_put (count=BUFFER_SIZE)
    Partial --> Empty: buffer_get (count=0)
    Full --> Partial: buffer_get
    
    note right of Empty
        消费者阻塞在 not_empty
        生产者可直接写入
    end note
    
    note right of Full
        生产者阻塞在 not_full
        消费者可直接读取
    end note

双条件变量设计

flowchart LR
    subgraph 生产者流程
        P1[获取锁] --> P2{队列满?}
        P2 -->|是| P3[wait not_full]
        P2 -->|否| P4[写入数据]
        P4 --> P5[signal not_empty]
        P5 --> P6[释放锁]
        P3 -.->|被唤醒| P4
    end
    
    subgraph 消费者流程
        C1[获取锁] --> C2{队列空?}
        C2 -->|是| C3[wait not_empty]
        C2 -->|否| C4[读取数据]
        C4 --> C5[signal not_full]
        C5 --> C6[释放锁]
        C3 -.->|被唤醒| C4
    end
    
    P5 -.->|唤醒| C3
    C5 -.->|唤醒| P3

核心技巧

技巧说明
双条件变量not_full + not_empty 分别处理两种等待场景
while 循环检查防止虚假唤醒和竞争条件,必须用 while 不能用 if
先加锁、再等待、最后解锁标准模式确保线程安全

3. 信号量实现(C++20 之前)

参考:C++20 标准 std::counting_semaphore 接口设计

代码实现

#include <mutex>
#include <condition_variable>

class semaphore {
private:
    std::mutex mtx;
    std::condition_variable cv;
    int count;  // 资源计数

public:
    explicit semaphore(int n = 0) : count(n) {}

    // P 操作:获取资源(阻塞)
    void acquire() {
        std::unique_lock<std::mutex> lock(mtx);
        cv.wait(lock, [this] { return count > 0; });
        --count;
    }

    // V 操作:释放资源
    void release() {
        std::lock_guard<std::mutex> lock(mtx);
        ++count;
        cv.notify_one();
    }

    // 尝试获取(非阻塞)
    bool try_acquire() {
        std::lock_guard<std::mutex> lock(mtx);
        if (count > 0) {
            --count;
            return true;
        }
        return false;
    }
};

资源管理模型

sequenceDiagram
    participant T1 as 线程A
    participant T2 as 线程B
    participant Sem as semaphore(count=1)
    participant Mutex as mutex
    participant CV as condition_variable

    Note over Sem: 初始 count=1
    
    T1->>Sem: acquire()
    Sem->>Mutex: lock
    Sem->>CV: wait (count>0)
    CV->>Mutex: unlock
    CV->>Sem: 条件满足
    Sem->>Mutex: lock
    Sem->>Sem: count-- (count=0)
    Sem->>Mutex: unlock
    
    T2->>Sem: acquire()
    Sem->>Mutex: lock
    Sem->>CV: wait (count>0)
    Note over T2: 阻塞等待...
    
    T1->>Sem: release()
    Sem->>Mutex: lock
    Sem->>Sem: count++ (count=1)
    Sem->>CV: notify_one()
    Sem->>Mutex: unlock
    
    CV->>T2: 被唤醒
    T2->>Mutex: lock
    T2->>Sem: count-- (count=0)
    T2->>Mutex: unlock

4. 读写锁(写者优先)

参考:操作系统教材中的读写锁经典算法

代码实现

#include <mutex>
#include <condition_variable>

class read_write_lock {
private:
    std::mutex mtx;
    std::condition_variable cv;
    
    int readers = 0;      // 当前读者数
    bool writing = false; // 是否有写者
    int wait_writers = 0; // 等待的写者数

public:
    // 读锁
    void read_lock() {
        std::unique_lock<std::mutex> lock(mtx);
        // 写者优先:有写者等待或正在写时,读者等待
        cv.wait(lock, [this] {
            return !writing && wait_writers == 0;
        });
        ++readers;
    }

    void read_unlock() {
        std::lock_guard<std::mutex> lock(mtx);
        --readers;
        if (readers == 0) {
            cv.notify_all();  // 唤醒等待的写者
        }
    }

    // 写锁
    void write_lock() {
        std::unique_lock<std::mutex> lock(mtx);
        ++wait_writers;
        cv.wait(lock, [this] {
            return !writing && readers == 0;
        });
        --wait_writers;
        writing = true;
    }

    void write_unlock() {
        std::lock_guard<std::mutex> lock(mtx);
        writing = false;
        cv.notify_all();  // 唤醒所有等待的读者和写者
    }
};

状态机图

stateDiagram-v2
    [*] --> Idle: 初始状态
    
    Idle --> Reading: read_lock (无写者等待)
    Idle --> Writing: write_lock (无读者)
    
    Reading --> Reading: read_lock (多个读者)
    Reading --> Idle: read_unlock (最后一个读者)
    
    Writing --> Idle: write_unlock
    
    Idle --> WriterWaiting: write_lock (有读者)
    WriterWaiting --> Writing: 最后一个读者离开
    WriterWaiting --> WriterWaiting: read_lock (被阻塞)
    
    note right of Reading
        多个读者可同时持有读锁
        新读者在有写者等待时被阻塞
    end note
    
    note right of WriterWaiting
        写者优先策略
        防止写者饥饿
    end note

5. Fast-DDS 中的实际应用

回到本文开头分析的 ResourceEvent::event_service(),它使用了双条件变量设计:

flowchart TD
    subgraph ResourceEvent 设计
        A[event_service 线程] --> B[处理定时器]
        B --> C{有待处理?}
        C -->|是| B
        C -->|否| D[allow_manipulation=true]
        D --> E[cv_manipulation.notify_all]
        E --> F[计算下次触发时间]
        F --> G[cv_.wait_until]
        G --> H[allow_manipulation=false]
        H --> B
    end
    
    subgraph 其他线程
        I[register_timer] --> J[cv_.notify_one]
        K[unregister_timer] --> L[等待 allow_manipulation]
        L --> M[操作集合]
        M --> N[cv_.notify_one]
    end
    
    E -.->|唤醒| L
    J -.->|唤醒| G
    N -.->|唤醒| G

设计对比

模式条件变量数使用场景
线程安全队列1单生产者-单消费者
生产者-消费者2有界缓冲区,需流量控制
ResourceEvent2复杂状态同步,需区分"业务唤醒"和"权限唤醒"

6. 生产环境推荐

需求教学代码生产推荐
线程安全队列threadsafe_queuemoodycamel::ConcurrentQueue
生产者-消费者buffer_tboost::lockfree::spsc_queue
信号量semaphorestd::counting_semaphore (C++20)
读写锁read_write_lockstd::shared_mutex (C++17)

7. 背诵要点

条件变量使用口诀

加锁之后判条件,条件不满足就等
等待自动放锁醒,醒来再判才放心
修改条件要加锁,改完通知莫忘记
通知之前可解锁,减少竞争提性能

关键代码模板

// 等待方
{
    std::unique_lock<std::mutex> lk(mut);
    cv.wait(lk, [&] { return 条件满足; });
    // 执行业务逻辑
}

// 通知方
{
    std::lock_guard<std::mutex> lk(mut);
    // 修改条件
    cv.notify_one();  // 或 notify_all()


参考资料

  1. 《C++ Concurrency in Action》第2版 - Anthony Williams
  2. 《UNIX环境高级编程》第3版 - W. Richard Stevens
  3. 《操作系统概念》第9版 - Silberschatz
  4. Fast-DDS 源码:src/cpp/rtps/resources/ResourceEvent.cpp