第4章:同步并发操作(C++并发编程实战)

本文介绍了C++中条件变量的两种实现std::condition_variable和std::condition_variable_any,一般首选前者。还说明了如何使用std::condition_variable处理等待的数据。此外,探讨了使用条件变量构建线程安全队列,包括队列操作、pop的变种等,指出条件变量并非在所有场景都是最佳同步机制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

4.1.1等待条件达成

C++标准对条件变量有两套实现:std::condition_variable和std::condition_variable_any。包含在<condition_variable>头文件声明中。两者都需要与一个互斥量一起才能工作;前者仅限于std::mutex一起工作,而后者可以和任何满足最低标准的互斥量一起工作,从而加上了_any后缀。因为std::condition_variable_any更加通用,这样体积、性能,以及系统资源的使用产生额外的开销。所以一般std::condition_variable作为首选,当对灵活性要求时候才考虑std::condition_variable_any。

如下使用std::condition_variable处理等待的数据:


std::mutex mut;
std::queue<data_chunk> data_queue;	//1
std::conditon_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);	//2
		data_cond.notify_one();	//3
	}
}

void data_processing_thread()
{
	while(true)
	{
		std::unique_lock<std::mutex> lk(mut);	//4
		data_cond.wait(lk,[]{return !data_queue.empty()});	//5
		data_chunk data = data_queue.front();
		data_queue.pop();
		lk.unlock();  //6
		process(data);
		if(is_last_chunk(data))
			break;
	}

}

首先,你拥有一个在两个线程之间传递数据的的队列1,当数据准备好,使用lock_guard对队列上锁,将数据压入队列中2,然后调用condition_variable的notify_one成员函数对等待的线程进行通知。

在另一侧,你有一个正在处理的数据的线程,这个线程首先对互斥量上锁,之后调用condition_variable的成员函数wait,传递一个锁和一个lambba表达式。当data_queue不为空——意味着数据准备好了。wait会去检查这个条件:当条件满足——即lambda返回true,如果条件不满足lamba返回false,将解锁互斥量,并且将这个线程置于阻塞或等待状态。使用unique_lock的原因:等待中的线程必须在等待期间解锁互斥量,并在这之后对互斥量继续上锁。

4.1.2 使用条件变量构建线程安全队列

设计一个通用队列的时候,可以看下标准库的的队列实现,如下是std::queue接口清单:


template<class T,class Container = std::deque<T> >
class queue
{
public:
	explicit queue(const Container&);
	explicit queue(Container&& = Container());
	template<class Alloc> explicit queue(const Alloc&);
	template<class Alloc>  queue(const Container&,const Alloc&);
	template<class Alloc>  queue(const Container&&,const Alloc&);
	template<class Alloc>  queue(queue&&,const Alloc&);	
	
	void swap(queue& q);
	
	bool empty() const;
	size_type size() const;
	
	T& front();
	const T& front() const;
	T& back();
	const T& back() const;
	
	void push(const T& x);
	void push(T&& x);
	void pop();
	template<class ... Args> void emplace(Args&& ... args);
};

忽略构造、赋值以及交换操作时候,你就剩三组操作:

  1. 对整个队列的状态进行查询(empty(),size());
  2. 查询队列的各个元素(front(),back());
  3. 修改队列的操作(push(),pop()和empace())。

这和构建线程安全栈一样,合并top和pop一样,你需要将front和pop合并成一个函数调用。与上述代码不同的是,当使用队列在多个线程中传递数据的时,接受线程通常需要等待数据的压入。这里提供了pop两个变种:try_pop和wait_and_pop。try_pop,尝试从队列弹出数据,总是直接返回(即使失败的时候);wait_and_pop将等待有值可检索的时候才返回。

如下是线程安全栈的接口:

#include <memory>

template<typename T>
class threadsafe_queue
{
public:
	threadsafe_queue();
	threadsafe_queue(const threadsafe_queue&);
	threadsafe_queue operator=(const threadsafe_queue&) = delete;//不允许简单的赋值
	
	void push();
	
	bool try_pop(T& value);	//1
	std::shared_ptr<T> try_pop();//2
	
	void wait_and_pop(T& value);
	std::shared_ptr<T> wait_and_pop();
	
	bool empty() const;
};

就像对栈做的一样,在这里你将很多构造函数剪掉,禁止了对队列的简单赋值。

和之前一样,你也需要提供两个版本的try_pop和wait_and_pop。第一个重载的try_pop1在引用了变量存储检索值,所以它可以用来返回队列中的值状态;第二个重载2不能这样做,因为它直接返回检索值的,当没有值可检索了,返回NULL。

将代码和之前的条件变量的代码合在一起,最开始的代码中提取push和wait_and_pop:

#include <queue>
#include <mutex>
#include <condition_variable>

template<typename T>
class threadsafe_queue
{
private:
	std::mutex mut;
	std::queue<data_chunk> data_queue;	
	std::conditon_variable data_cond;
public:
	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();
	}
};

threadsafe_queue<data_chunk> data_queue;	//1

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);	//2
	}
}

void data_processing_thread()
{
	while(true)
	{
		data_chunk data;
		data_queue.wait_and_pop(data); //3
		process(data);
		if(is_last_chunk(data))
			break;
	}

}

线程队列的实例包含了互斥量和条件变量,所以独立的变量就不需要 了1,并且调用push也不需要外部同步2,wait_and_pop还要兼顾条件量的等待。

如下是完整的使用条件变量的线程安全队列:

#include <queue>
#include <memory>
#include <mutex>
#include <condition_variable>

template<typename T>
class threadsafe_queue
{
private:
	mutable std::mutex mut;		//1互斥量必须是可变的
	std::queue<T> data_queue;	
	std::conditon_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::unique_lock<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::unique_lock<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();
	}
};

当等待线程只等待一次,当条件为true,它就不等待条件变量了,所以一个条件变量可能并非同步机制最好的选择。尤其是,条件在等待一组可用的数据块时,这种情况下就要选择期望(future)。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值