C++ 多线程学习(3) ---- 条件变量

1. 条件变量简介 

在 C++ 11中,我们可以使用条件变量(condition variable)实现多个线程之间的同步操作,当条件不满足时,相关线程一直被阻塞,直到某种条件成立,这些线程才会被唤醒。

条件变量是利用线程间共享的全局变量进行同步的一种机制,主要包含两个动作:

  • 一个线程因为等待条件变量的条件成立而挂起,
  • 另外一个线程使条件成立,给出信号,从而唤醒被等待的线程。

为了防止竞争,条件变量总是和一个互斥锁结合在一起,通常情况下这个锁是 std::mutex,并且管理这个锁的只能是 std::unique_lock<std::mutex> RAII 的模板类。

原子操作的概念:

所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。

临界资源:

临界资源是一次执行过程仅仅允许一个进程使用的共享资源,各个进程采取互斥的方式实现共享,属于临界资源的硬件有 打印机,磁带机等,软件有消息队列,变量,数组,缓冲区等,各个进程采取互斥的方式实现对这种资源的共享。(可以理解为对资源的一次操作不能被打断,也就是一次操作过程需要完整,不能操作出现中间切走的情况)

临界区:

每个进程访问临界资源的那段代码称为临界区(criticalsection),每次只允许一个进程进去临界区,进去后,不允许其他进程进入。不论是硬件临界资源还是软件临界资源,多个进程必须互斥的对它进行访问,使用临界区时,一般不允许其运行时间过长,只要运行在临界区的线程还没有离开,其他所有进入此临界区的线程都会被挂起而进入等待状态,并在一定程度上影响程序的运行性能。

2. 条件变量的使用接口

上面提到的两个步骤,分别用下面的方法实现:

等待条件成立使用的是 condition_variable 类成员函数 wait,wait_for 和 wait_unitl

给出信号的使用的是 condition_variable 类成员函数 notify_one 和 notify_all 函数

2.1 wait/wait_for 函数

wait 导致当前线程阻塞直至条件变量被通知,或虚假唤醒发生,可选地循环直至满足某谓词

wait_for 函数导致当前的线程阻塞直到条件变量被通知,或者虚假唤醒发生,或者超时返回。

返回值说明:

1. 如果经过 rel_time 指定的关联时限则为 std:cv_status:timeout,否则为 std:cv_status:no_timeout

以上两个类型的 wait 函数都会在阻塞的时候,自动释放锁的权限,即调用 unique_lock 的成员函数 unlock(),以便于其他线程能够有机会获得锁,这就是条件变量只能和 unique_lock 一起使用的原因,否则线程一直占有锁,线程被阻塞。

2.2 notify/notify_one

notify/notify_one 函数声明如下:

notify_one:任何线程在 *this 上等待,则调用 notify_one 会解阻塞(唤醒)等待的线程之一

notify:唤醒任何在 *this 上等待的线程

在正常的情况下,wait 类型函数返回时要不是因为被唤醒,要不是因为超时才返回,但是在实际中发现,因为操作系统的原因,wait 类型在不满足条件时,它也会返回,这就导致了虚假唤醒,因此我们一般都是使用带有谓词参数的 wait 函数。

condition_variable 的一般用法如下:

  • 如果两个线程共享的变量存在其中一个线程读取,另一个线程写入的情况,或者存在两个线程都要写的情况,那么它们共享的变量需要在互斥锁的保护之下。
  • notify 函数本身也需要在互斥锁的保护之下
  • wait 函数本身就带有一个互斥锁的参数
condition_variable threadqueueDemo::cv;
mutex threadqueueDemo::mDisplayMutex;
queue<int> threadqueueDemo::mDisplayQueue;

void threadqueueDemo::threadLoop() {
	    while (true) {
		{
		    unique_lock<mutex> lock(mDisplayMutex);
			if (mDisplayQueue.empty()) {
				cout << "thread tid = " << this_thread::get_id() << endl;
				cv.wait(lock);
			} else {
				auto p = mDisplayQueue.front();
				mDisplayQueue.pop();
				cout << "get data p =" << p << endl;
			}
		}
	}
}

threadqueueDemo::threadqueueDemo() {
	t = new thread(threadLoop);
	t->detach();

	for (int i = 0; i < 20; i++) {
		pushdata();
		this_thread::sleep_for(chrono::milliseconds(500ms));
	}
}

void threadqueueDemo::pushdata() {
	unique_lock<mutex> lock(mDisplayMutex);
	{
		++mData;
		mDisplayQueue.push(mData);
    	cv.notify_all();
	}
}


void threadqueueDemo::testthreadqueue(int argc, char* argv[]) {
	threadqueueDemo* t = new threadqueueDemo;
}

3. 生产者消费问题

在这里,我们使用条件变量,解决生产者-消费者问题,该问题的主要描述如下:

生产者-消费这个问题,也称为有限缓冲问题,是一个进程/线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个进程/线程—— 就是所谓的生产者和消费者,在实际运行时会发生的问题。

生产者的主要作用是生成一定量的数据放到缓冲区,然后重复此过程。与此同时,消费者也在缓冲区中消耗这些数据。该问题的关键就是要保证生产者不会再缓冲区满时加入数据,消费者也不会在缓冲区空时消耗数据。

要解决该问题,就必须让生产者在缓冲区满时休眠,等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。

同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据知乎,再唤醒消费者。

示例代码如下:

std::mutex g_cvMutex;
std::condition_variable g_cv;


std::deque<int> g_data_deque;
const int  MAX_NUM = 30;
int g_next_index = 0;

const int PRODUCER_THREAD_NUM = 3;
const int CONSUMER_THREAD_NUM = 3;

void producer_thread(int thread_id)
{
	while (true) {
		std::this_thread::sleep_for(std::chrono::milliseconds(500));
		std::unique_lock<mutex> lk(g_cvMutex);
		if (g_data_deque.size() <= MAX_NUM) {
			g_next_index++;
			g_data_deque.push_back(g_next_index);
			std::cout << "producer_thread: " << thread_id << " producer data: " << g_next_index;
			std::cout << " queue size: " << g_data_deque.size() << std::endl;
		} else {
			g_cv.notify_all();
		}
	}
}


void consumer_thread(int thread_id) {
	while (true) {
		std::this_thread::sleep_for(std::chrono::milliseconds(500));
		std::unique_lock <std::mutex> lk(g_cvMutex);
		if (!g_data_deque.empty()) {
			int data = g_data_deque.front();
			g_data_deque.pop_front();
			std::cout << "\tconsumer_thread: " << thread_id << " consumer data: ";
			std::cout << data << " deque size: " << g_data_deque.size() << std::endl;
		} else {
			g_cv.wait(lk);
		}
	}
}


producerconsumer::producerconsumer() {

	std::thread *producerthread[PRODUCER_THREAD_NUM];
	std::thread *consumerthread[CONSUMER_THREAD_NUM];


	for (int i = 0; i < PRODUCER_THREAD_NUM; i++) {
		producerthread[i] = new thread(producer_thread, ref(i));
	}

	for (int j = 0; j < CONSUMER_THREAD_NUM; j++) {
		consumerthread[j] = new thread(consumer_thread, ref(j));
	}
	
	for (int i = 0; i < PRODUCER_THREAD_NUM; i++) {
		producerthread[i]->join();
	}

	for (int j = 0; j < CONSUMER_THREAD_NUM; j++) {
		consumerthread[j]->join();
	}
}
  • 程序中创建三个生产者线程,每个线程都会向 g_data_deque 这个双端队列中 push 数据
  • 三个生产者线程push 的 int 类型使用的是全局变量,是在已有的基础上累加的 int 类型变量
  • 程序中创建了三个消费者线程,只要双端队列不为空,就会将数据从双端队列中 pop 出来
  • 生产者消费者会有抢占的操作,就是消费者并没有完全将队列消费完,让队列为空的时候,生产者再次将生产的内容放入队列,相当于有6个线程,在g_cvMutex 的保护下,生产和消费队列里面的内容

 执行结果如下:

4. 为什么条件变量要和互斥锁一起使用

首先要说明为什么要引入条件变量,如果某个线程需要等待某个条件成立,而这个条件又是其他线程给出的,其中的一种解决方案是:

threadA() {

lock
if(条件满足)
unlock
    excute code;
} else {
    sleep(n);
}
}
  • 如果一直判断条件是否满足,线程没有阻塞等待,是非常浪费CPU资源的,所以引入了 sleep 函数,相当于 sleep 一段时间后再做判断
  • 注意此时的判断条件是临界资源,需要锁的保护

引入条件变量后,若条件不满足,则相应线程被阻塞直至条件发生变化被唤醒,再去查询条件是否满足,避免了上述条件变化发生之前的无用查询包括加解锁

条件变量的引入,使多个线程以一种无竞争的方式等待条件的改变

在使用 condition_variable 的 wait 函数之前,通常要进行条件判断,而此条件属于临界资源(在读取此条件的时候不能被打断,比如去更新这个条件),需要在访问之前加锁,这是为了保护临界资源的需要。

unique_lock<mutex> lock(mDisplayMutex);
if (mDisplayQueue.empty()) {

	cv.wait(lock);
}		

比如上面的代码中如果不加锁保护,在判断 mDisplayQueue.empty() 的条件成立后,此时线程被挂起,调度了另一个线程,另外的线程用于唤醒这个 condition_variable,此时当前线程因为还没有处于 wait 状态上(没有处于调度器的等待队列上),所以会丢失掉这个唤醒操作,如果这个唤醒操作只有一次,那么当前线程很可能永远处于阻塞等待的状态上。

所以 condtion_variable 加互斥锁的作用,就是保证全局条件和wait 的操作是原子操作

C++面试问题:为什么条件变量要和互斥锁一起使用?_条件变量为什么要和锁一起用_August8757的博客-CSDN博客

  • 26
    点赞
  • 52
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,你想了解关于 C++ 条件变量多线程中的使用。 在多线程中,条件变量通常用于线程间的同步,它可以让一个线程等待另一个线程的特定事件发生。当这个特定事件发生时,另一个线程可以通过条件变量来通知等待的线程,让它继续执行。 C++ 中的条件变量一般需要和互斥锁一起使用,以保证线程安全。当一个线程等待条件变量时,它会释放互斥锁,让其他线程可以继续执行,等到条件变量被唤醒后,线程会重新获得互斥锁并继续执行。 下面是一个简单的示例代码: ```cpp #include <iostream> #include <thread> #include <mutex> #include <condition_variable> std::mutex mtx; std::condition_variable cv; bool ready = false; void worker_thread() { // do some work { std::lock_guard<std::mutex> lk(mtx); ready = true; } cv.notify_one(); // 通知等待的线程 } int main() { std::thread worker(worker_thread); // 等待条件变量 { std::unique_lock<std::mutex> lk(mtx); cv.wait(lk, []{ return ready; }); // 等待条件变量被唤醒 } std::cout << "Worker thread finished\n"; worker.join(); return 0; } ``` 在这个示例中,我们新开了一个线程 `worker_thread`,它会在一段时间后设置条件变量 `ready` 为 `true`,然后通知等待的线程。在 `main` 函数中,我们首先获得了互斥锁 `mtx`,然后调用 `cv.wait` 等待条件变量被唤醒。在等待期间,线程会释放互斥锁,让 `worker_thread` 可以继续执行。等到 `worker_thread` 设置了条件变量并通知后,`main` 函数会重新获得互斥锁并继续执行。最后,我们等待 `worker_thread` 结束并回收资源。 希望这个示例可以帮助你理解条件变量多线程中的使用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值