zl程序教程

您现在的位置是:首页 >  后端

当前栏目

C++ | 多线程编程(三) 条件变量

2023-09-27 14:28:31 时间

条件变量:https://zh.cppreference.com/w/cpp/thread/condition_variable

condition_variable 类是同步原语,能用于阻塞一个线程,或同时阻塞多个线程,直至另一线程修改共享变量(条件)并通知 condition_variable 。
在这里插入图片描述
条件变量一般与互斥锁一起使用。

三个线程轮流打印0~100.

目标:是三个线程分别依次打印1,2,3…100

#include <iostream>
#include <thread>
#include <mutex>				// 互斥量
#include <condition_variable>	// 条件变量

using namespace std;


/*
	// 版本一:不加信号量,互斥量,也可以完成,但会造成“空耗cpu”
	例如在fun0中,不满足number%3==0条件,
	则一直进行循环,直至线程时间片到达,切换线程
*/
void fun0()	// 0  3
{
	while (number <= 100)
	{
		while (number % 3 == 0 && number <= 100)
		{
			cout << "t0_" << number << " ";
			number += 1;
		}
		//cout << ".";
	}
}
void fun1()	// 1  4
{
	while (number <= 100)
	{
		while (number % 3 == 1 && number <= 100)
		{
			cout << "t1_" << number << " ";
			number += 1;
		}
		//cout << ".";
	}
}
void fun2()	// 2  5
{
	while (number <= 100)
	{
		while (number % 3 == 2 && number <= 100)
		{
			cout << "t2_" << number << " ";
			number += 1;
		}
		//cout << ".";
	}
}

int main()
{
	thread t0(fun0);
	thread t1(fun1);
	thread t2(fun2);

	t0.join();
	t1.join();
	t2.join();

	return 0;
}

在这里插入图片描述

缺点:会使得CPU空耗大量时间。取消上边的cout << ".";注释可以看到cpu空耗情况。
在这里插入图片描述

使用条件变量

条件变量:
	notify_one()(随机唤醒一个等待的线程)
	notify_all()(唤醒所有等待的线程)

	wait()是条件变量的成员函数,堵塞,等待唤醒
		如果第二个参数lambda表达式返回值是false,
		那么wait将解锁第一个参数(互斥量),并堵塞到本行。
	堵塞到其他某个线程调用notify_one()成员函数为止

	notify_one();//尝试吧wait的线程唤醒,执行完这行,wait就被唤醒了
	当其他notify_one()将wait唤醒之后,wait不断尝试获取互斥量锁,
	如果获取不到,流程就卡在wait这里等着获取。
	如果获取到了,wait就获取到锁(就等于上锁);

这里存在一个问题,在下面的程序片段中,我们有Aa方案和Bb方案使用条件变量。

void fun0()	// 0  3
{
	unique_lock < std::mutex> locker(g_mtx);	// 唯一性锁
	while (number <= 100)
	{
		while (number % 3 == 0 && number <= 100)
		{
			cout << "t0_" << number << " ";
			number += 1;
//			cv.wait(locker);	// 等待  A  ,A-a 先等待后唤醒
			// 阻塞,等待其他线程唤醒
			cv.notify_one();	// 唤醒	 B  ,B-b 先唤醒后等待
			// 通知别的线程,唤醒其他线程
		}
		cout << ".a";
//		cv.notify_one();	// 唤醒  a
		cv.wait(locker);	// 等待  b
	}
	cv.notify_one();	// 唤醒最后一个阻塞线程
}

等待和唤醒调用的时机很重要,如果我们使用Aa方案,那么可能会出现两种情况。

  1. 第一个执行到此处的线程将会进入等待状态,而其他线程因为没要满足进入循环的条件将会空耗时间。
  2. 三个线程刚好顺序执行,则三个线程最后都陷入了等待状态,但是由于没有额外的线程进行唤醒,会使得程序陷入循环等待状态。
    在这里插入图片描述

在这里插入图片描述
因此,我们需要使用Bb方案。

/*
	// 版本二:加锁
*/
void fun0()	// 0  3
{
	unique_lock < std::mutex> locker(g_mtx);	// 唯一性锁
	while (number <= 100)
	{
		while (number % 3 == 0 && number <= 100)
		{
			cout << "t0_" << number << " ";
			number += 1;
			cv.notify_one();	// 唤醒	 B  ,B-b 先唤醒后等待
			// 通知别的线程,唤醒其他线程
		}
		cout << ".";
		cv.wait(locker);	// 等待  b
	}
	cv.notify_one();	// 唤醒最后一个阻塞线程
}
void fun1()	// 1  4
{
	unique_lock < std::mutex> locker(g_mtx);
	while (number <= 100)
	{
		while (number % 3 == 1 && number <= 100)
		{
			cout << "t1_" << number << " ";
			number += 1;
			cv.notify_one();
		}
		cout << ".";
		cv.wait(locker);
	}
	// 打印100,最后阻塞线程为fun1
	cv.notify_one();
}
void fun2()	// 2  5
{
	unique_lock < std::mutex> locker(g_mtx);
	while (number <= 100)
	{
		while (number % 3 == 2 && number <= 100)
		{
			cout << "t2_" << number << " ";
			number += 1;
			cv.notify_one();
		}
		cout << ".";
		cv.wait(locker);
	}
	cv.notify_one();
}

int main()
{
	thread t0(fun0);
	thread t1(fun1);
	thread t2(fun2);

	t0.join();
	t1.join();
	t2.join();

	return 0;
}

在这里插入图片描述
此方案依然存在问题。

  • 问题1:唤醒的线程,如果不是我们需要的线程,就会空耗被唤醒线程直至再次唤醒其他线程,这个过程一直到我们需要的线程被唤醒。(有可能出现死锁情况)
    // 在程序中,输出两个‘.’表示有空耗
  • 解决方案:使用 notify_all 唤醒所有其他线程
  • 问题2:最后一个线程使用wait后,阻塞,不会有其他线程将其唤醒
  • 解决方案:在最后一个线程wait时,在其他的任意一个可以正常退出的线程中调用一次唤醒程序,比如打印100个数,fun1最后退出,可以在fun0,或fun2中调用一次唤醒函数

如下图所示出现死锁情况:
在这里插入图片描述

修改方案,使用notify_all唤醒


/*
	// 版本三:加锁
	使用notify_all
	线程退出时,调用一次唤醒函数
*/
void fun0()	// 0  3
{
	unique_lock < std::mutex> locker(g_mtx);	// 唯一性锁
	while (number <= 100)
	{
		while (number % 3 == 0 && number <= 100)
		{
			cout << "t0_" << number << " ";
			number += 1;
			//			cv.wait(locker);	// 等待  A  ,A-a 先等待后唤醒
						// 阻塞,等待其他线程唤醒
			cv.notify_all();	// 唤醒	 B  ,B-b 先唤醒后等待
			// 通知别的线程,唤醒其他线程
		}
		cout << ".";
		//		cv.notify_one();	// 唤醒  a
		cv.wait(locker);	// 等待  b
	}
	cv.notify_all();	// 唤醒最后一个阻塞线程
}
void fun1()	// 1  4
{
	unique_lock < std::mutex> locker(g_mtx);
	while (number <= 100)
	{
		while (number % 3 == 1 && number <= 100)
		{
			cout << "t1_" << number << " ";
			number += 1;
			cv.notify_all();
		}
		cout << ".";
		cv.wait(locker);
	}
	// 打印100,最后阻塞线程为fun1
	cv.notify_all();
}
void fun2()	// 2  5
{
	unique_lock < std::mutex> locker(g_mtx);
	while (number <= 100)
	{
		while (number % 3 == 2 && number <= 100)
		{
			cout << "t2_" << number << " ";
			number += 1;
			cv.notify_all();
		}
		cout << ".";
		cv.wait(locker);
	}
	cv.notify_all();
}

int main()
{
	thread t0(fun0);
	thread t1(fun1);
	thread t2(fun2);

	t0.join();
	t1.join();
	t2.join();

	return 0;
}

在这里插入图片描述