C++多线程交替打印详解:从互斥锁到条件变量的实现
在C++多线程编程中,交替打印是考察线程同步机制的经典场景。本文将演示如何通过互斥锁和条件变量,让两个线程按顺序分别打印奇数和偶数,避免数据竞争并实现精确的并发控制。
目标:实现两个线程交替打印
我们的最终目标是创建两个线程,让它们依次打印数字序列,例如一个打印奇数(1, 3, 5...),另一个打印偶数(2, 4, 6...),直到达到指定数量。为了实现这一目标,我们需要处理线程间的资源竞争和调度顺序。
第一步:基础框架与问题分析
首先,我们尝试用最基础的 <thread>库创建两个线程来打印计数器。由于两个线程同时访问共享变量 i,且没有同步机制,会导致数据二义性和输出混乱。
第1步:创建基础并发框架
- 入口:
#include <iostream>,#include <thread> - 动作:在主函数中创建两个
std::thread对象,并使用 Lambda 表达式捕获外部变量n(上限)和i(当前值)。 - 关键点:两个线程使用
t.join()等待结束。
// 基础框架:两个线程争夺打印 i
#include <iostream>
#include <thread>
using namespace std;
int main(void)
{
int n = 100;
int i = 0;
// 创建两个线程,共同操作 i
thread t1([&n, &i](){
while (i < n)
{
cout << i << " ";
i++;
}
});
thread t2([&n, &i]() {
while (i < n)
{
cout << i << " ";
i++;
}
});
if (t1.joinable()) t1.join();
if (t2.joinable()) t2.join();
return 0;
}
此时运行结果往往不是有序的 0, 1, 2...,而是出现跳跃或重复,因为 i 是临界资源,多个线程争抢访问造成数据不一致。为了看清是哪个线程在打印,我们可以添加 this_thread::get_id() 来标识线程身份:
// 增加线程ID以观察执行主体
#include <iostream>
#include <thread>
using namespace std;
int main(void)
{
int n = 100;
int i = 0;
thread t1([&n, &i](){
while (i < n)
{
cout << this_thread::get_id() << ": " << i << endl;
i++;
}
});
thread t2([&n, &i]() {
while (i < n)
{
cout << this_thread::get_id() << ": " << i << endl;
i++;
}
});
if (t1.joinable()) t1.join();
if (t2.joinable()) t2.join();
return 0;
}
第二步:使用互斥量保护临界资源
为了解决数据安全问题,必须确保同一时刻只有一个线程能访问 i。我们可以引入 <mutex> 互斥量进行加锁。为了保证两个线程使用同一把锁,需要在 main 函数中定义 mutex mtx,并通过引用传递给 Lambda。
第2步:加入 Mutex 进行同步
- 操作:在每次访问
i前调用mtx.lock(),访问后调用mtx.unlock()。 - 参数:
mutex mtx作为全局唯一的锁对象。
#include <iostream>
#include <thread>
#include <mutex>
using namespace std;
int main(void)
{
int n = 100;
int i = 0;
mutex mtx;
thread t1([&n, &i, &mtx](){
while (i < n)
{
mtx.lock();
cout << this_thread::get_id() << ": " << i << endl;
i++;
mtx.unlock();
}
});
thread t2([&n, &i, &mtx]() {
while (i < n)
{
mtx.lock();
cout << this_thread::get_id() << ": " << i << endl;
i++;
mtx.unlock();
}
});
if (t1.joinable()) t1.join();
if (t2.joinable()) t2.join();
return 0;
}
虽然此时线程安全了,但如果某个线程竞争力较强,可能会连续抢到锁,导致另一个线程饥饿,依然无法实现“交替”打印。此外,手动 lock/unlock 容易因异常导致死锁,C++推荐由 RAII 类管理锁。
第三步:引入 unique_lock 与管理类
C++提供了 lock_guard 和 unique_lock 两种管理类。lock_guard 仅用于简单的构造/析构加锁,而 unique_lock 更灵活,支持条件变量的 wait 操作。我们将使用 unique_lock 来管理锁。
// 模拟 lock_guard 原理
template <class Lock>
class LockGuard
{
public:
LockGuard(Lock &lck) : _lock(lck) { _lock.lock(); }
~LockGuard() { _lock.unlock(); }
private:
Lock &_lock;
};
// 实际使用中直接使用 std::unique_lock
第四步:使用条件变量实现交替逻辑
要实现严格的交替打印,需要引入 condition_variable(条件变量)和一个状态标志 flag。基本逻辑是:当前线程执行完后修改 flag,唤醒对方线程;对方线程在等待时检查 flag,条件满足则继续,否则阻塞。
第4步:配置 Condition Variable 和 Flag
- 准备条件:引入
<condition_variable>,定义bool flag = false初始状态。 - 动作:在线程中使用
cv.wait(lock, predicate)阻塞等待。
int main(void)
{
int n = 100;
int i = 0;
mutex mtx;
condition_variable cv;
bool flag = false; // 初始为false,让t2先等还是t1先等取决于逻辑设计
// 线程t1:打印奇数或根据flag逻辑执行
thread t1([&n, &i, &mtx, &cv, &flag](){
while (i < n)
{
unique_lock<mutex> LockManage(mtx);
// 等待 !flag 为真才继续
cv.wait(LockManage, [&flag]() {return !flag; });
cout << this_thread::get_id() << ": " << i << endl;
i++;
flag = true; // 修改状态
cv.notify_one(); // 唤醒等待者
}
});
// 线程t2:打印偶数
thread t2([&n, &i, &mtx, &cv, &flag]() {
while (i < n)
{
unique_lock<mutex> LockManage(mtx);
// 等待 flag 为真才继续
cv.wait(LockManage, [&flag]() {return flag; });
cout << this_thread::get_id() << ": " << i << endl;
i++;
flag = false; // 还原状态
cv.notify_one(); // 唤醒等待者
}
});
if (t1.joinable()) t1.join();
if (t2.joinable()) t2.join();
return 0;
}
在上述代码中,flag 的初始值为 false。
- t1 在 cv.wait 中判断 !flag(即 true),所以 t1 不阻塞,先获得锁执行打印。
- t2 在 cv.wait 中判断 flag(即 false),条件不满足,进入阻塞队列。
当 t1 执行完一次打印后,设置 flag = true 并调用 cv.notify_one()。此时 t2 被唤醒,重新获取锁,发现 flag 为真,于是跳出等待执行打印。
第五步:验证与结果
通过这种“互斥锁+条件变量+状态标志”的组合,我们实现了严格的交替执行。每个线程只在自己该执行时运行,打印完毕后立即交出控制权。
最终输出将呈现有序的递增序列,且每个数字只由一个线程输出一次,达到了两个线程交替打印的目的。这种方式比单纯的信号量或睡眠轮询更加高效和稳定。
以上就是C++详解如何实现两个线程交替打印的详细内容,更多关于C++线程并发控制的资料请关注本站其它相关文章!
本站声明
本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。













