多线程编程:线程间的同步与通信
一、概述
在多线程编程中,必须解决以下两个核心问题:
- 线程间的互斥:确保共享资源在同一时刻只被一个线程访问,防止数据竞争。
- 线程间的同步通信:使线程之间按照指定顺序执行,以实现任务协调和依赖关系。
二、线程间的互斥
1. 静态条件与临界区
- 静态条件:多线程程序在不同执行顺序下可能产生不一致的结果。
- 临界区 :代码中访问共享资源的部分。
- 解决方法:每次只能允许一个线程进入临界区。
2. 互斥锁(Mutex)
- 作用 :
- 保证临界区代码段的原子性。
- 防止多个线程同时访问共享资源。
- 实现方式 :
- 使用
std::mutex
加锁和解锁。 - 对于较小的临界区,可以使用轻量级的无锁机制(如 CAS)。
- 使用
三、线程间的同步通信
1. 同步通信的必要性
- 多线程程序中,线程的执行顺序由操作系统调度决定,通常是不确定的。
- 在某些场景下,线程需要依赖其他线程的执行结果。例如:
- 生产者-消费者问题:生产者需要将数据生产完毕后通知消费者进行消费。
2. 条件变量(Condition Variable)
- 作用:实现线程间的同步通信。
- 特点 :
- 需要与
std::mutex
搭配使用。 - 提供
wait
和notify
方法,实现线程间的协调。
- 需要与
四、生产者-消费者模型
1. 问题描述
生产者线程负责生产数据,并将数据放入队列;消费者线程负责从队列中取出数据并消费。
- 目标 :
- 保证生产者和消费者交替工作。
- 防止队列为空时消费者消费数据或队列溢出时生产者继续生产。
2. 代码实现
全局变量
cpp
std::queue<int> q; // 共享队列
std::mutex mtx; // 互斥锁
std::condition_variable cv; // 条件变量
生产者函数
cpp
void producer() {
for (int i = 1; i <= 10; ++i) {
std::unique_lock<std::mutex> lock(mtx); // 加锁
q.push(i); // 生产数据
std::cout << "Produced: " << i << std::endl;
cv.notify_all(); // 通知消费者
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟生产时间
}
}
消费者函数
cpp
void consumer() {
for (int i = 1; i <= 10; ++i) {
std::unique_lock<std::mutex> lock(mtx); // 加锁
cv.wait(lock, [] { return !q.empty(); }); // 等待队列非空
int value = q.front(); // 获取数据
q.pop(); // 移除数据
std::cout << "Consumed: " << value << std::endl; // 打印消费日志
lock.unlock(); // 解锁
cv.notify_all(); // 通知生产者
std::this_thread::sleep_for(std::chrono::milliseconds(100)); // 模拟消费时间
}
}
主函数
cpp
int main() {
std::thread t1(producer); // 创建生产者线程
std::thread t2(consumer); // 创建消费者线程
t1.join(); // 等待生产者线程完成
t2.join(); // 等待消费者线程完成
return 0;
}
3. 核心逻辑
同步通信的实现
-
生产者和消费者的协调:
- 生产者生产数据后通过
cv.notify_all()
通知消费者。 - 消费者消费数据后通过
cv.notify_all()
通知生产者。
- 生产者生产数据后通过
-
防止竞争和死锁:
- 使用
std::mutex
确保队列的操作线程安全。 cv.wait
确保消费者只有在队列非空时才执行操作。
- 使用
4. 状态变化分析
事件 | 状态 | 行为 |
---|---|---|
消费者发现队列为空 | 阻塞 | 等待生产者生产数据后被通知 |
生产者生产数据 | 队列非空,通知消费者 | 消费者从等待状态转为阻塞状态,等待获取锁继续执行 |
消费者消费数据 | 队列可能为空,通知生产者 | 生产者从等待状态转为阻塞状态,等待获取锁继续执行 |
6. 注意事项
-
线程安全:
- 队列的所有操作(如
push
和pop
)必须在加锁状态下进行。
- 队列的所有操作(如
-
条件变量使用:
wait
必须传入一个std::unique_lock<std::mutex>
对象。notify_all
应用于多个消费者时,notify_one
适用于单一消费者场景。
-
死锁防范:
- 确保每个线程在进入
wait
状态前释放锁。
- 确保每个线程在进入
五、总结
-
互斥锁的作用:
- 确保线程安全,避免共享资源的并发访问导致的数据竞争。
-
条件变量的作用:
- 线程间的同步通信,协调线程的执行顺序。
-
核心操作:
std::mutex
:加锁与解锁。std::condition_variable
:线程等待(wait
)与通知(notify_all
和notify_one
)。
-
生产者-消费者模型的实现:
- 使用共享队列进行数据传递。
- 条件变量协调线程之间的依赖顺序。