[modern c++] std::condition_variable 条件变量的使用

机制:

wait 函数之前需要先对条件变量的保护mutex 加锁。

当调用wait时,会依次进行如下三个动作:

1)wait会把自己的判断条件注册给内核(如果使用void wait( std::unique_lock<std::mutex>& lock, Predicate stop_waiting ););

2)wait会释放刚才持有的 保护mutex ;

3)接着阻塞住,等待被 notify 通知或者等待条件满足。如果调用的是带谓词的wait,那么在wait之前谓词就已经满足,则不需要等待被notify也能直接运行下去。如果调用的是不带谓词的wait,那么只能被notify来跳出wait。

当notify 发出通知时:

1)内核通知wait条件已经满足,但是wait还不会返回。

当notify线程notify后并解锁保护mutex时:

1)各个wait线程的wait函数在内部竞争保护mutex;

2)竞争到mutex的wait线程的wait函数返回。

当前线程的wait能够返回要同时满足三个条件:1)notify的通知到达了;2)notify的线程释放了保护mutex;3)当前线程在wait内部竞争到了保护mutex。

注:如果notify_all,则所有wait线程会依次获得锁,所谓的依次是上一个wait线程释放了保护mutex的时候,剩下的继续竞争,直到所有的wait线程都从wait返回或者因为wait条件无法得到满足而再次wait阻塞。




nofity解析:

notify_one的原语是通知一个 wait线程跳出wait状态,所有线程第一时间会竞争抢夺锁的控制权,获得控制权的线程有权利继续下一步,其他线程继续投入wait睡眠。

notify_all的原语是通知所有 wait线程跳出wait状态,如果是带谓词的wait则需要判断谓词是否满足,如果不满足则投入wait睡眠;如果是不带谓词的wait那么所有wait线程都会跳出wait继续运行。

notify_one 场景1:

#include <iostream>
#include <string>
#include <thread>
#include <mutex>
#include <condition_variable>
#include <stdlib.h>
#include <unistd.h>

std::mutex m;
std::condition_variable cv;
std::string data;
bool ready = false;

void worker_thread()
{
    std::unique_lock<std::mutex> lk(m);
    cv.wait(lk, []{return ready;});
    std::cout << "wait retain lock\n";
}

int main()
{
    std::thread worker(worker_thread);

    sleep(2);  //make sure worker_thread step into wait statement

    {
      //due to wait will release mutex, so here lock will success.
      std::lock_guard<std::mutex> lk(m);

      //prepare condition
      ready = true;

      //notify wait thread
      cv.notify_one();

      std::cout << "wait for 5s, \n";

/*hold mutex m for 5 seconds, this will block the wait for executing, so even though condition is meet, but wait will still blocked for 5 seconds*/

      sleep(5);    
    }

    worker.join();
}

上面的例子中,wait的条件已经被满足了,但是worker thread还是会等待5秒后才跳出wait语句,因为main thread 拿着 mutex m sleep 了5 秒,这个时候 worker thread的wait语句实际上在等待 mutex m 的释放等了5 秒。

记住,wait线程进入wait语句的下一条语句时,是持有保护mutex的。获取动作在wait内部执行。

notify_one 场景2:

#include <thread>
#include <mutex>
#include <list>
#include <unistd.h>
#include <stdio.h>
#include <condition_variable>
#include <sys/types.h>

std::list<long> FIFO;
std::mutex lock;
std::condition_variable cv;
long consumer_v = -1;
long producer_v = 99999;

void consumer(){
        static long times=0;
        while(consumer_v!=0){
                std::unique_lock<std::mutex> lk(lock);
                cv.wait(lk,[]{return !FIFO.empty();});
                consumer_v = std::move(FIFO.front());
                FIFO.pop_front();
                lk.unlock();
                times++;
                printf("[%d]consumer times : %ld\n" ,gettid(), times);
        }
}

void producer(){
        static long times=0;
        while(producer_v--!=0){
                sleep(5);
                std::unique_lock<std::mutex> lk(lock);
                FIFO.push_back(producer_v);
                FIFO.push_back(producer_v);
                FIFO.push_back(producer_v);
                FIFO.push_back(producer_v);
                FIFO.push_back(producer_v);
                FIFO.push_back(producer_v);
                cv.notify_one();
                lk.unlock();
                times++;
                printf("producer times : %ld\n" , times);
        }
}

int main()
{
        std::thread cons(consumer);
        std::thread cons1(consumer);
        std::thread prod(producer);
        cons.join();
        prod.join();
}

上述代码的输出为:

producer times : 1
[2042078]consumer times : 1
[2042078]consumer times : 2
[2042078]consumer times : 3
[2042078]consumer times : 4
[2042078]consumer times : 5
[2042078]consumer times : 6

可以看到,同一个消费者线程连续获得了6次机会,这说明带谓词的wait只要谓词能够满足则不需要等待notify。这里另外一个消费者线程因为没有抢到notify_one的消息而继续处于睡眠状态,进而丧失了6次资源争夺机会。

notify_one 场景3:

#include <thread>
#include <mutex>
#include <list>
#include <unistd.h>
#include <stdio.h>
#include <condition_variable>
#include <sys/types.h>

std::list<long> FIFO;
std::mutex lock;
std::condition_variable cv;
long consumer_v = -1;
long producer_v = 99999;

void consumer(){
        static long times=0;
        while(consumer_v!=0){
                std::unique_lock<std::mutex> lk(lock);
                cv.wait(lk,[]{return !FIFO.empty();});
                consumer_v = std::move(FIFO.front());
                FIFO.pop_front();
                lk.unlock();
                times++;
                printf("[%d]consumer times : %ld\n" ,gettid(), times);
        }
}

void producer(){
        static long times=0;
        while(producer_v--!=0){
                sleep(5);
                std::unique_lock<std::mutex> lk(lock);
                FIFO.push_back(producer_v);
                cv.notify_one();
                FIFO.push_back(producer_v);
                cv.notify_one();
                FIFO.push_back(producer_v);
                cv.notify_one();
                FIFO.push_back(producer_v);
                cv.notify_one();
                FIFO.push_back(producer_v);
                cv.notify_one();
                FIFO.push_back(producer_v);
                cv.notify_one();
                lk.unlock();
                times++;
                printf("producer times : %ld\n" , times);
        }
}

int main()
{
        std::thread cons(consumer);
        std::thread cons1(consumer);
        std::thread prod(producer);
        cons.join();
        prod.join();
}

上述代码的输出为:

producer times : 1
[2044302]consumer times : 1
[2044303]consumer times : 2
[2044303]consumer times : 3
[2044303]consumer times : 4
[2044303]consumer times : 6
[2044302]consumer times : 5

这一次,两个消费者线程都得到了跳出wait的机会,这是因为进行了6次notify_one,每个线程都有6次机会跳出wait函数且继续往下运行。

notify_all:

#include <thread>
#include <mutex>
#include <list>
#include <unistd.h>
#include <stdio.h>
#include <condition_variable>
#include <sys/types.h>

std::list<long> FIFO;
std::mutex lock;
std::condition_variable cv;
long consumer_v = -1;
long producer_v = 99999;

void consumer(){
        static long times=0;
        while(consumer_v!=0){
                std::unique_lock<std::mutex> lk(lock);
                cv.wait(lk,[]{return !FIFO.empty();});
                consumer_v = std::move(FIFO.front());
                FIFO.pop_front();
                lk.unlock();
                times++;
                printf("[%d]consumer times : %ld\n" ,gettid(), times);
        }
}

void producer(){
        static long times=0;
        while(producer_v--!=0){
                sleep(10);
                std::unique_lock<std::mutex> lk(lock);
                FIFO.push_back(producer_v);
                FIFO.push_back(producer_v);
                FIFO.push_back(producer_v);
                FIFO.push_back(producer_v);
                FIFO.push_back(producer_v);
                cv.notify_all();
                lk.unlock();
                times++;
                printf("producer times : %ld\n" , times);
        }
}

int main()
{
        std::thread cons(consumer);
        std::thread cons1(consumer);
        std::thread prod(producer);
        cons.join();
        prod.join();
}

 从如下结果可以看到,当有充足的资源时,notify_all会让所有wait线程都有机会跳出wait等待状态。如果改成notify_one,则只会有一个线程有机会跳出wait等待。

producer times : 1
[2041004]consumer times : 1
[2041003]consumer times : 2
[2041003]consumer times : 4
[2041003]consumer times : 5
[2041003]consumer times : 6
[2041004]consumer times : 3

notify_all会同时唤醒所有wait的线程,但是如果想让所有线程都能执行下去而不是再次投入睡眠,则需要notify_all的时候,每个线程的wait条件能够得到满足,比如有2个消费者线程同时wait,而队列中只有一条数据,这个时候只有一个线程会在被唤醒后继续执行下去,另外一个在被唤醒后会因为队列为空而再次被投入睡眠。




wait解析:

有谓词的wait 和 没有谓词的wait 会导致两种完全不同的代码逻辑,一定不能将他们等效对待。上一章节的notify_one场景2中,如果wait都不指定谓词,那么wait是没有机会连续进入多次的。

#include <thread>
#include <mutex>
#include <list>
#include <unistd.h>
#include <stdio.h>
#include <condition_variable>
#include <sys/types.h>

std::list<long> FIFO;
std::mutex lock;
std::condition_variable cv;
long consumer_v = -1;
long producer_v = 99999;

void consumer(){
        static long times=0;
        while(consumer_v!=0){
                std::unique_lock<std::mutex> lk(lock);
                cv.wait(lk);
                consumer_v = std::move(FIFO.front());
                FIFO.pop_front();
                lk.unlock();
                times++;
                printf("[%d]consumer times : %ld\n" ,gettid(), times);
        }
}

void producer(){
        static long times=0;
        while(producer_v--!=0){
                sleep(5);
                std::unique_lock<std::mutex> lk(lock);
                FIFO.push_back(producer_v);
                FIFO.push_back(producer_v);
                FIFO.push_back(producer_v);
                FIFO.push_back(producer_v);
                FIFO.push_back(producer_v);
                FIFO.push_back(producer_v);
                cv.notify_one();
                lk.unlock();
                times++;
                printf("producer times : %ld\n" , times);
        }
}

int main()
{
        std::thread cons(consumer);
        std::thread cons1(consumer);
        std::thread prod(producer);
        cons.join();
        prod.join();
}

运行结果:

producer times : 1
[2045968]consumer times : 1




注意:

  • 条件变量的信号不会排队,如果发送信号的一方在发送信号时还没有任何线程通过 wait 等待信号,那么这次信号会被丢弃(类比Windows下Event的脉冲模式,与之相反的是Windows下Event的电平模式)。
  • wait 跳出阻塞是并不 100% 保证条件已经满足,我们还需要二次确认。因为有时候会出现spurious wakeup。所以最完美的写法是如下两种之一:
while (!stop_waiting()) {
    wait(lock);
}

或者

wait(lock,[]{return stop_waiting()});
  • wait 有其他两个版本,一个是 wait_for,一个是 wait_until ,二者都是用来附加一个超时机制,当超时时间到达则会跳出 wait 阻塞,这个时候我们需要人为判断是不是条件得到满足了。
  • 当wait条件满足的时候,wait跳出阻塞向下执行(在wait执行完毕并进入下一条语句之前,wait会再次持有锁,但是在wait等待期间并不会持有锁),此时wait所在的那个线程是持有锁的,发送信号线程如果再次对锁加锁将被阻塞住。
  • notify_one不会阻塞也不要求必须加锁才能使用,发送信号的线程只会在试图加锁时被阻塞。但是,如果加锁后再使用notify可以一定程度上提高效率。



代码片段:

std::mutex mut;
std::queue<data_chunk> data_queue;
std::condition_variable data_cond;

void data_preparation_thread()
{
	while (more_data_to_prepare())
	{
		data_chunk const data = prepare_data();
		std::lock_guard<std::mutex> lk(mut);
		data_queue.push(data);
		data_cond.notify_one();
	}
}
void data_processing_thread()
{
	//下面两种场景是等同的,这里使用unique_lock的目的是让data_preparation_thread不在mut的获取上长时间阻塞,因为process(data);语句可能会非常耗时间
	//如果不想用unique_lock,可以使用 {} 增加一个作用域来 人为触发 lock_guard 的析构,从而在 process(data); 之前释放 mut
#if 1
	while (true)
	{
		std::unique_lock<std::mutex> lk(mut);
		data_cond.wait(lk, [] {return !data_queue.empty(); });
		data_chunk data = data_queue.front();
		data_queue.pop();
		lk.unlock();
		process(data);
		if (is_last_chunk(data))
			break;
	}
#else
	while (true)
	{
		{
			std::lock_guard<std::mutex> lk(mut);
			data_cond.wait(lk, [] {return !data_queue.empty(); });
			data_chunk data = data_queue.front();
			data_queue.pop();
		}
		process(data);
		if (is_last_chunk(data))
			break;
	}
#endif
}



使用条件变量的线程安全队列:

#include <queue>
#include <memory>
#include <mutex>
#include <condition_variable>
template<typename T>
class threadsafe_queue
{
private:
	mutable std::mutex mut;
	std::queue<T> data_queue;
	std::condition_variable data_cond;
public:
	threadsafe_queue()
	{}
	threadsafe_queue(threadsafe_queue const& other)
	{
		std::lock_guard<std::mutex> lk(other.mut);
		data_queue = other.data_queue;
	}
	void push(T new_value)
	{
		std::lock_guard<std::mutex> lk(mut);
		data_queue.push(new_value);
		data_cond.notify_one();
	}
	void wait_and_pop(T& value)
	{
		std::unique_lock<std::mutex> lk(mut);
		data_cond.wait(lk, [this] {return !data_queue.empty(); });
		value = data_queue.front();
		data_queue.pop();
	}
	std::shared_ptr<T> wait_and_pop()
	{
		std::unique_lock<std::mutex> lk(mut);
		data_cond.wait(lk, [this] {return !data_queue.empty(); });
		std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
		data_queue.pop();
		return res;
	}
	bool try_pop(T& value)
	{
		std::lock_guard<std::mutex> lk(mut);
		if (data_queue.empty())
			return false;
		value = data_queue.front();
		data_queue.pop();
		return true;
	}
	std::shared_ptr<T> try_pop()
	{
		std::lock_guard<std::mutex> lk(mut);
		if (data_queue.empty())
			return std::shared_ptr<T>();
		std::shared_ptr<T> res(std::make_shared<T>(data_queue.front()));
		data_queue.pop();
		return res;
	}
	bool empty() const
	{
		std::lock_guard<std::mutex> lk(mut);
		return data_queue.empty();
	}
};



模拟WINDOWS的Event:

#include <iostream>
#include <condition_variable>
#include <mutex>
#include <memory>
#include <thread>
#include <Windows.h>

std::mutex m;
std::condition_variable cv;

void func1() {
	while (1) {
		std::unique_lock<std::mutex> l(m);
		cv.wait(l);
		std::cout << "func1"<<std::endl;
	}
}

void func2() {
	while (1) {
		std::unique_lock<std::mutex> l(m);
		cv.wait(l);
		std::cout << "func2" << std::endl;
	}
}


void func3() {
	while (1) {
		std::unique_lock<std::mutex> l(m);
		cv.wait(l);
		std::cout << "func3" << std::endl;
	}
}


int main()
{
	std::thread t1(func1);
	std::thread t2(func2);
	std::thread t3(func3);


	while (1) {
		std::unique_lock<std::mutex> l(m);
		cv.notify_all();
		l.unlock();
		Sleep(1000);
		std::cout << "================\n";
	}

    std::cout << "Hello World!\n";
	while (1);
}

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值