C++双线程交替打印奇偶数(活泼版)
文章目录
- C++双线程交替打印奇偶数(活泼版)
- 1.🎮 游戏规则说明书
- 2.🔧 游戏道具准备区
- 2.1🧩 道具清单
- 3.👯♂️ 创建两个线程小伙伴
- 3.1🧑💻 线程A(奇数打印机)
- 👩💻 线程B(偶数打印机)
- 4.🔍原理解释(为什么可以这么写)
- 4.1🔒 第一关:共享数据保护(为什么用mutex?)
- 问题场景
- 解决方案:互斥锁(mutex)
- 4.2第二关:线程通信(为什么用condition_variable?)
- 问题场景
- 解决方案:条件变量+谓词判断
- 4.3🔄 第三关:执行顺序控制(为什么这样设计判断逻辑?)
- 线程A(奇数线程)的逻辑
- 线程B(偶数线程)的逻辑
- 5.💡 关键知识点泡泡
- 5.1🧠 互斥锁(mutex)
- 5.2🧠 条件变量(condition_variable)
- 5.3🧠 unique_lock
- 6.🚨 常见bug救护车
- ⚠️ 忘记notify
- ⚠️ 虚假唤醒
- ⚠️ 锁的粒度
- 7.完整代码展示
- 8.🎯 趣味扩展挑战
今天我们要玩一个超酷的"数字接龙"游戏——用两个线程交替打印奇偶数!就像两个同学轮流报数,一个说"1",另一个马上接"2",快跟着我一起来看看这个神奇的代码魔术吧!✨
今天需要提前铺垫的知识有:
1.线程的基本用法
2.mutex锁的玩法
3.条件变量的简单玩法
对于上述三点前置知识不太清楚的童鞋可以提前看咱们的第五大点:关键知识点泡泡!
1.🎮 游戏规则说明书
游戏目标:创建两个线程小伙伴:
-
线程A专门打印奇数(1、3、5…)
-
线程B专门打印偶数(0、2、4…)
-
它们要像打乒乓球一样轮流工作,直到数到100!
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
using namespace std;
// 准备好我们的"游戏道具"啦!
2.🔧 游戏道具准备区
2.1🧩 道具清单
int x = 0; // 我们的"接力棒"数字
int n = 100; // 终点线数字
mutex mtx; // 互斥锁(防止抢答的小喇叭)
condition_variable cv; // 条件变量(线程的"对讲机")
-
接力棒(x):两个线程要传递的数字
-
小喇叭(mtx):防止两个线程同时说话(数据竞争)
-
对讲机(cv):让线程能互相通知"该你啦!"
3.👯♂️ 创建两个线程小伙伴
3.1🧑💻 线程A(奇数打印机)
thread t1([&, n]() {
while (x < n) {
unique_lock<mutex> lock(mtx); // 抓住小喇叭
if (x % 2 == 0) { // 如果发现是偶数
cv.wait(lock); // 放下喇叭睡觉:"轮到偶数了?叫我!"
}
cout << "线程" << this_thread::get_id() << ":" << x << endl;
x++; // 数字+1
cv.notify_one(); // 喊醒线程B:"该你啦!"
}
});
👩💻 线程B(偶数打印机)
thread t2([&, n]() {
while (x < n) {
unique_lock<mutex> lock(mtx); // 抓住小喇叭
if (x % 2 != 0) { // 如果发现是奇数
cv.wait(lock); // 放下喇叭睡觉:"轮到奇数了?叫我!"
}
cout << "线程" << this_thread::get_id() << ":" << x << endl;
x++; // 数字+1
cv.notify_one(); // 喊醒线程A:"该你啦!"
}
});
游戏开始啦!
t1.join(); // 等待线程A完成任务
t2.join(); // 等待线程B完成任务
🤔 超有趣的工作原理图解
初始状态:x=0(偶数)
- 线程B先工作(因为x是偶数,线程A在睡觉)
- 打印0 → x变成1 → 叫醒线程A
- 线程A被唤醒:
- 打印1 → x变成2 → 叫醒线程B
- 线程B被唤醒:
- 打印2 → x变成3 → 叫醒线程A
…(循环直到x=100)
4.🔍原理解释(为什么可以这么写)
核心问题分析
我们需要解决三个关键问题:
-
共享数据保护:两个线程同时操作
x
会引发数据竞争 -
执行顺序控制:必须严格交替执行(奇-偶-奇-偶…)
-
线程通信机制:一个线程完成后要准确通知另一个线程
4.1🔒 第一关:共享数据保护(为什么用mutex?)
问题场景
-
线程A正在读取
x
的值 -
同时线程B正在修改
x
的值 -
→ 导致数据不一致!
解决方案:互斥锁(mutex)
unique_lock<mutex> lock(mtx); // 进入"VIP室",独享操作权
-
比喻:就像洗手间的门锁🚪,一个人进去后会自动锁门,其他人必须等待
-
关键点:
-
使用
unique_lock
而不是lock_guard
(因为后面需要手动解锁) -
锁的范围要刚好覆盖共享数据的操作区域
-
4.2第二关:线程通信(为什么用condition_variable?)
问题场景
-
线程A(奇数线程)发现当前是偶数时,不能简单循环等待
-
否则会一直占用CPU资源(忙等待)
解决方案:条件变量+谓词判断
if (x % 2 == 0) { // 谓词判断
cv.wait(lock); // 释放锁并等待通知
}
-
工作原理:
-
原子地释放锁并进入等待状态
-
被唤醒后自动重新获取锁
-
-
为什么不用sleep?
-
sleep是盲等,无法精确响应状态变化
-
condition_variable是精准的事件通知机制
-
4.3🔄 第三关:执行顺序控制(为什么这样设计判断逻辑?)
线程A(奇数线程)的逻辑
if (x % 2 == 0) // 发现是偶数就等待
cv.notify_one(); // 打印完奇数后叫醒偶数线程
线程B(偶数线程)的逻辑
if (x % 2 != 0) // 发现是奇数就等待
cv.notify_one(); // 打印完偶数后叫醒奇数线程
-
精妙之处:
-
每个线程只处理自己该处理的状态
-
通过数值奇偶性自然形成状态切换
-
notify相当于"传球"动作
-
5.💡 关键知识点泡泡
5.1🧠 互斥锁(mutex)
就像"只有一个人能说话的小喇叭"🎤,防止两个线程同时修改共享数据x
5.2🧠 条件变量(condition_variable)
线程们的"智能对讲机"📞:
-
wait()
:“我去睡觉啦,有消息再叫我” -
notify_one()
:“醒醒!该你干活啦!”
5.3🧠 unique_lock
智能锁管家,离开作用域会自动释放锁,再也不用担心忘记解锁啦!
6.🚨 常见bug救护车
⚠️ 忘记notify
如果线程执行完不叫醒对方,另一个线程会永远睡觉(死锁)💤
⚠️ 虚假唤醒
线程可能莫名其妙自己醒了,所以判断条件要用while而不是if(虽然我们这里if也够用啦)
⚠️ 锁的粒度
锁的范围太大(比如包住整个while循环)会导致性能下降哦!
7.完整代码展示
#include<iostream>
#include<thread>
#include<mutex>
#include<condition_variable>
#include<functional>
using namespace std;
int main()
{
//控制两个线程 ,实现一个线程打印奇数,一个进程打印偶数
int x = 0;
int n = 100;
condition_variable cv;
mutex mtx;
thread t1([&, n]()
{
while (x < n)
{
unique_lock<mutex> lock(mtx);
if (x % 2 == 0) //偶数就阻塞
{
cv.wait(lock);
}
cout << this_thread::get_id() << "号进程" << ":" << x << endl;
x++;
cv.notify_one();//唤醒一个锁
}
}
);
thread t2([&, n]()
{
while (x < n)
{
unique_lock<mutex> lock(mtx);
if (x % 2 != 0) //奇数就阻塞
{
cv.wait(lock);
}
cout << this_thread::get_id() << "号进程" << ":" << x << endl;
x++;
cv.notify_one();//唤醒一个锁
}
}
);
t1.join();
t2.join();
return 0;
}
8.🎯 趣味扩展挑战
学会了咱们这个玩法之后可以尝试:
-
试试三个线程交替打印1、2、3?
-
改成打印字母A、B、C…Z怎么样?
-
添加颜色输出,让奇数和偶数显示不同颜色!🌈
看完这篇是不是觉得多线程超有趣?就像指挥两个乖巧的同学完美配合!快去试试这个代码吧~遇到问题欢迎在评论区留言,我会像notify_one()
一样第一时间唤醒回复你!😊
记得点赞⭐收藏哟~