C++11的条件变量

        条件变量是C++11提供的另外一种用于等待的同步机制,它能阻塞一个或多个线程,直到收到另外一个线程发出的通知或者超时,才会唤醒当前阻塞的线程。条件变量需要和互斥量配合起来用。C++11提供了两种条件变量:

        1、condition_variable,配合std::unique_lock<std::mutex>进行wait操作。

        2、condition_variable_any,和任意带有lock,unlokc语义的mutex搭配使用,比较灵活,但效率比condition_variable差一些。

        可以看到condition_variable_any比condition_variable更灵活,因为它更通用,对所有的锁都适用,而condition_variable性能更好。

        条件变量的使用过程如下:

        1、拥有条件变量的线程获取互斥量。

        2、循环检查某个条件,如果条件不满足,则阻塞直到条件满足;如果条件满足,则向下执行。

        3、某个线程满足条件执行完之后调用notify_one或notify_all唤醒一个或者所有的等待线程。

        可以用条件变量来实现一个同步队列,同步队列作为一个线程安全的数据共享区,经常用于线程之间数据读取,比如半同步半异步线程池的同步队列。

#include <mutex>
#include <thread>
#include <condition_variable>
#include <list>
#include <string>
#include <iostream>
using namespace std;

template<typename T>
class SyncQueue
{
	bool IsFull() const
	{
		return m_queue.size() == m_maxSize;
	}

	bool IsEmpty() const
	{
		return m_queue.empty();
	}

public:
	SyncQueue(int maxSize) : m_maxSize(maxSize){}

	void Put(const T& x)
	{
		std::lock_guard<std::mutex> locker(m_mutex);
		while(IsFull())
		{
			cout << "缓冲区满了,需要等待..." << endl;
			m_notFull.wait(m_mutex);
		}

		m_queue.push_back(x);
		m_notEmpty.notify_one();
	}

	void Take(T& x)
	{
		std::lock_guard<std::mutex> locker(m_mutex);
		while(IsEmpty())
		{
			cout << "缓冲区空了,需要等待..." << endl;
			m_notEmpty.wait(m_mutex);
		}

		x = m_queue.front();
		m_queue.pop_front();
		m_notFull.notify_one();
	}

	int Count()
	{
		return m_queue.size();
	}

	bool Empty()
	{
		std::lock_guard<std::mutex> locker(m_mutex);

		return m_queue.empty();
	}

	bool Full()
	{
		std::lock_guard<std::mutex> locker(m_mutex);

		return m_queue.size() == m_maxSize;
	}

private:
	std::list<T>	m_queue;					///缓冲区
	std::mutex		m_mutex;					///互斥量和条件变量结合起来使用
	std::condition_variable_any m_notEmpty;		///不为空的条件变量
	std::condition_variable_any m_notFull;		///没有满的条件变量
	int							m_maxSize;		///同步队列最大的size
};

SyncQueue<std::string> myqueue(1024);

void send_thr()
{
	int i = 0;
	char strbuf[100] = {0};
	std::chrono::milliseconds sleepDuration(2000);
	while(1)
	{
		snprintf(strbuf, sizeof(strbuf), "send %d", ++i);
		myqueue.Put(strbuf);
		std::this_thread::sleep_for(sleepDuration);
		if (i > 1000)
		{
			i = 0;
		}
	}
}

void recv_thr()
{
	std::string str;
	std::chrono::milliseconds sleepDuration(1000);

	while(1)
	{
		myqueue.Take(str);
		cout << str << endl;

		std::this_thread::sleep_for(sleepDuration);
	}
}

int main()
{
	thread t1(send_thr);
	thread t2(recv_thr);

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

	return 0;
}

        这个同步队列在没有满的情况下可以插入数据,如果满了,则会调用m_notFull阻塞等待,待消费线程取出数据之后发一个未满的通知,然后前面阻塞的线程就会被唤醒继续往下执行;如果队列为空,就不能取数据,会调用m_notempty条件变量阻塞,等待插入数据的线程发出不为空的通知时,才能继续往下执行。

        上述代码用到了std::lock_guard,它利用了RAII机制可以保证安全释放mutex。从上面代码还可以看到,std::unique_lock和std::lock_guard的差别在于前者可以自由的释放mutex,而后者则需要等待std::lock_guard变量声明周期结束时才释放。条件变量的wait还有一个重载方法,可以接受一个条件。比如下面的代码:

std::lock_guard<std::mutex> locker(m_mutex);
while(IsFull())
{
    m_notFull.wait(m_mutex);
}

        可以改为这样:

std::lock_guard<std::mutex> locker(m_mutex);
m_notFull.wait(locker, [this]{ return !IsFull();});

        两种写法效果是一样的,但是后者更简洁,条件变量先检查判断式是否满足条件,如果满足条件,则重新获取mutex,然后结束wait,继续往下执行;如果不满足条件,则释放mutex,然后将线程置为waiting状态,继续等待。

        这里需要注意的是,wait函数中会释放mutex,而lock_guard这时还拥有mutex,它只会在出了作用域之后才会释放mutex,所以,这时它并不会释放,但执行wait时会提前释放mutex。从语义上看这里使用lock_guard会产生矛盾,但是实际上并不会出问题,因为wait提前释放锁之后处于等待状态,在被notify_one或者notify_all唤醒之后会先获取mutex,这相当于lock_guard的mutex在释放之后又获取到了,因此,在出了作用域之后lock_guard自动释放mutex不会出问题。这里应该有unique_lock,因为unique_lock不像lock_guard一样只能在析构时才释放锁,它随时释放锁,因此,在wait时让unique_lock释放锁从语义上看更加准确。

        我们可以修改下上面的代码,把std::lock_guard改成std::unique_lock,把sd::condition_variable_any改为std::condition_variable,并且用等待一个判断式的方法来实现一个简单的线程池。

#include <mutex>
#include <thread>
#include <condition_variable>
#include <list>
#include <string>
#include <iostream>
using namespace std;

template<typename T>
class SyncQueue
{
	bool IsFull() const
	{
		return m_queue.size() == m_maxSize;
	}

	bool IsEmpty() const
	{
		return m_queue.empty();
	}

public:
	SyncQueue(int maxSize) : m_maxSize(maxSize){}

	void Put(const T& x)
	{
		std::lock_guard<std::mutex> locker(m_mutex);
		m_queue.push_back(x);
		m_notEmpty.notify_one();
	}

	void Take(T& x)
	{
		std::unique_lock<std::mutex> locker(m_mutex);
		m_notEmpty.wait(locker, [this]{return !m_queue.empty(); });
		x = m_queue.front();
		m_queue.pop_front();
	}

	int Count()
	{
		return m_queue.size();
	}

	bool Empty()
	{
		std::lock_guard<std::mutex> locker(m_mutex);

		return m_queue.empty();
	}

	bool Full()
	{
		std::lock_guard<std::mutex> locker(m_mutex);

		return m_queue.size() == m_maxSize;
	}

private:
	std::list<T>	m_queue;					///缓冲区
	std::mutex		m_mutex;					///互斥量和条件变量结合起来使用
	std::condition_variable m_notEmpty;		///不为空的条件变量
	int						m_maxSize;		///同步队列最大的size
};

SyncQueue<std::string> myqueue(1024);

void send_thr()
{
	int i = 0;
	char strbuf[100] = {0};
	std::chrono::milliseconds sleepDuration(2000);
	while(1)
	{
		snprintf(strbuf, sizeof(strbuf), "value %d", ++i);
		cout << "send " << strbuf << endl;
		myqueue.Put(strbuf);
		std::this_thread::sleep_for(sleepDuration);
		if (i > 1000)
		{
			i = 0;
		}
	}
}

void recv_thr()
{
	std::string str;
	std::chrono::milliseconds sleepDuration(1000);

	while(1)
	{
		myqueue.Take(str);
		cout << "recv " << str << endl;

		std::this_thread::sleep_for(sleepDuration);
	}
}

int main()
{
	thread t1(send_thr);
	thread t2(recv_thr);

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

	return 0;
}

        相比于上面的例子,这次使用unique_lock代替lock_guard,使语义更加准确,用性能更好的condition_variable替代condition_variable_any,对程序加以优化,这里仍然用condition_variable_any也是可以的。执行wait时不再通过while循环来判断,而是通过lambda表达式来判断,写法上更简洁了。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值