6 ———— 条件变量std::condition_variable

条件变量std::condition_variable

文章目录

    • 条件变量std::condition_variable
      • 什么是条件变量?
      • std::condition_variable、notify_one()、wait()
      • notify_all()

什么是条件变量?


  线程间的同步有这样一种情况:线程A需要等某个条件成立才能继续往下执行,现在这个条件不成立,线程A就阻塞等待,而线程B在执行过程中使这个条件成立了,于是唤醒线程A继续执行。条件变量就是用在这种场合,它让一个线程等条件变量而挂起,另一个线程在条件满足时再发送信号去通知等待的线程。

  最典型的一个问题是生产者与消费者问题:两个线程共享一个公共的固定大小的缓冲区。其中一个是生产者,用于将数据放入缓冲区,持续进行。另一个是消费者,用于从缓冲区中读取数据,持续进行。当缓冲区满时,此时如果生产者必须等待消费者取出一个数据后才能继续存放数据,必须等待。当缓冲区空时,如果消费都还想去取数据,要等生产者存放一个数据才能去取,这段时间也必须等待。

  假设用一个全局变量count表示当前缓冲区中的数据个数,N表示缓冲区的总大小。线程A充当消费者,线程B充当生产者。当缓冲区为空时,线程A 读取count = 0,准备休眠(sleep)。而此时线程B打算生产数据,当生产一个数据时,由于 count + 1,由此推断线程A已经休眠,所以线程B会发一个信号给线程A,让其恢复消费,继续运行。由于线程A与B没有对count进行互斥量加锁,所以可能A准备休眠之前,线程B给线程A发了信号(count已经不为0),但是由于线程A读取的还是count = 0 (没有加锁),导致信号被忽略。所以A继续休眠,B一直生产。最终,B也因为缓冲区满,也休眠。线程相互等待,造成死锁。

  因此,条件变量必须绑定一个互斥量,当条件不满足时,进行如下原子操作:线程将mutex解锁、线程被条件变量阻塞。这是一个原子操作,不会被打断。当条件变量状态改变,被通知时(条件不一定满足,如虚假唤醒),重新判断当前条件是否满足,对互斥量加锁,进行操作,再解锁。

std::condition_variable、notify_one()、wait()

1 std::condition_variable 是一个类模板,仅限于与 std::mutex 一起工作
2 只有使用std::unique_lock对mutex进行管理,因为在条件变量wait()期间,要对mutex解锁。

#include 
#include 
#include 
#include 


using namespace std;


class A
{
public:
	// 把收到的消息传入队列
	void inMsgRecvQueue()
	{
		for (size_t i = 0; i < 1000; ++i)
		{
			cout << "收到消息,并放入队列 " << i << endl;
			
			std::unique_lock my_uniq(my_mutex);
			msgRecvQueue.push_back(i);
			my_condi.notify_one(); // 我们尝试把my_condi.wait()唤醒,通知它。
		}

		cout << "消息入队结束" << endl;
	}

	// 从队列中取出消息
	void outMsgRecvQueue()
	{
		while (true)
		{
			// 获取my_mutex,并加锁
			std::unique_lock my_uniq(my_mutex);

			//	wait()等待一个通知和一个条件。如果第二个参数lambda表达示返回值是false,那么wait()将解锁互斥量。并堵塞到本行。
			//	堵塞到什么时候为止呢?堵塞到其它某个线程调用notify_one()成员函数为止;
			//	如果wait()没有第二个参数:my_condi.wait(my_uniq),  那么就跟第二个参数lambda表达示返回false一样。

			//	当其它线程用notify_one()通知本线程时,wait就开始恢复工作,wait()不断尝试,重新获取互斥量的锁。
			//	如果获取不到锁,流程就卡在wait()这,继续等待加锁互斥量。

			//	如果wait有第二个参数(lambda),就判断lambda表达式,如果lambda表达式返回false,那么wait又对互斥量解锁。继续等待下一次notify
			//	如果lambda表达式返回true,则wait()返回,流程往下走。
			//	如果wait没有第二个参数,则wait()返回,流程走下来。
			my_condi.wait(my_uniq, [this] {
				if (!msgRecvQueue.empty())
					return true;
				else
					return false;
			});

			// 如果流程走到这来,互斥量一定是加锁的。
			int command = msgRecvQueue.front();
			msgRecvQueue.pop_front();


			my_uniq.unlock();	//	提前解锁,避免锁住太长时间
			//	其它业务

			cout << "出队列:" << command << endl;

		}

	}

private:
	list msgRecvQueue;	
	mutex my_mutex;	
	std::condition_variable my_condi;	//	生成一个条件对象
};


int main()
{
	A myobj;
	thread	myInMsgObj(&A::inMsgRecvQueue, &myobj); 
	thread	myOutMsgObj(&A::outMsgRecvQueue, &myobj);
	myInMsgObj.join();
	myOutMsgObj.join();

	return 0;
}

上述代码中隐藏的问题:
  由于条件变量要一直等待 inMsgRecvQueue()线程中通知,并且,即使收到知道后,还需要去获取my_mutex的加锁,才能继续走outMsgRecvQueue()的流程。这样就会导致,即使两个线程同时运行,会使msgRecvQueue队列中存入了大量了数据,而只取了少量的数据。最终,存数据线程已经走完,而取数据线程并没有取出其中所有的数据。

notify_all()

  notify_one()只能通知另外的一个线程去尝试获取锁,在有多个线程等待条件变量的情况下,只能有一个线程能去尝试获取锁,唤醒哪个线程,不能确定。但是,notify_all()时,其它的所有线程都会唤醒,有机会去获取mutex加锁,但是只有一个线程能获取锁成功。

你可能感兴趣的:(C++并发)