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 | 有界缓冲区,需流量控制 |
| ResourceEvent | 2 | 复杂状态同步,需区分"业务唤醒"和"权限唤醒" |
6. 生产环境推荐
| 需求 | 教学代码 | 生产推荐 |
|---|---|---|
| 线程安全队列 | threadsafe_queue | moodycamel::ConcurrentQueue |
| 生产者-消费者 | buffer_t | boost::lockfree::spsc_queue |
| 信号量 | semaphore | std::counting_semaphore (C++20) |
| 读写锁 | read_write_lock | std::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()
参考资料
- 《C++ Concurrency in Action》第2版 - Anthony Williams
- 《UNIX环境高级编程》第3版 - W. Richard Stevens
- 《操作系统概念》第9版 - Silberschatz
- Fast-DDS 源码:
src/cpp/rtps/resources/ResourceEvent.cpp