4.2 使用期望等待一次性事件(C++并发编程实战)

期望(future):当一个线程需要等待一个特定的一次性事件,在某种程度上来说它就需要知道这个时间的未来表现形式。之后,这个线程会周期性(较短的)的等待和检查,事件是否触发;在检查期间也会执行其他的任务。另外,在等待期间它可以先执行另外一些任务,知道对应的任务触发,而后等待期望的状态会变为就绪(ready)。一个期望可能是数据相关,也可能不是。当事件发生(并且期望状态就绪),这个期望就不能被重置。

C++标准库中有两种期望:唯一期望(std::futures<>)和共享期望(std::shared_futures<>)。仿照unique_ptr和shared_ptr。唯一期望的实例只能与一个指定事件相关,而共享期望的实例能关联多个事件。后者的实现中,所有实例会同时变为就绪状态,并且它们可以访问与事件相关的任何数据。线程中通讯的时,期望对象本身并不提供同步访问,当多个线程需要访问一个独立期望时,它们必须使用同步机制保护。

4.2.1 带返回值的后台任务

假设你有一个需要长时间的运算,你需要能计算出一个有效值,但你现在不迫切需要。你可以启用新线程来执行这个运算,需要获取返回值,你可以使用std::async函数模板,因为std::thread并不提供直接返回接受值的机制。

std::async允许你传递额外的参数:当第一个参数是指向成员函数的指针,第二个参数是这个函数成员类的具体对象(通过指针,还可以使用std::ref),剩下的参数可作为成员函数的参数传入。

清单4.6 使用std::future从异步任务中获取返回值:

#include <iostream>
#include <future>

int find_the_answer_to_ltuae();
void do_other_stuff();

int main()
{
	std::future<int> the_anser
		= std::async(find_the_answer_to_ltuae);
	do_other_stuff();
	std::cout << "the answer is" << the_anser.get() << std::endl;
}

清单4.7 使用std::async向函数传递参数

#include <future>
#include <string>

struct X
{
	void foo(int,std::string const&);
	std:: string bar(std::string const&);
};
X x;
auto f1 = std::async(&X::foo,&x,42,"hello"); //调用p->foo(42,"hello"); p是指向x的指针
auto f2 = std::async(&X::bar,x,"goodbye");	//调用temp.bar("goodbye"),tempx是x的拷贝副本

struct Y
{
	double operator()(double);
};
Y y;
auto f3 = std::async(Y(),3.14);	//调用tempy(3.14).tempy通过Y的移动构造函数得到
auto f4 = std::async(std::ref(y),2.718);	//调用y(2.718)

X baz(X&);
std::async(barz,std::ref(x));	//调用baz(x);

class move_only
{
	move_only();
	move_only(move_only&&);
	move_only(const move_only&) = delete;
	move_only& operator=(move_only&&);
	move_only& operator=(const move_only&) = delete;
	
	void operator()();
};

auto f5 = std::async(move_only());	//调用temp(),temp是通过std::move(move_only())构造得到

默认情况下,期望是否进行等待取决于std::async是否启动一个线程,或是否由任务正在进行同步。大多数情况下,但是你也可以在函数调用之前,向std::async传递一个额外参数,这个参数类型是std::launch,还可以是std::launch::deferred。用来表面函数调用延迟到wait或get函数调用时才执行。std::launch::async,表面必须在其所在的独立线程执行,std::launch::deferred和std::launch::async表明实现可以选择这两种方式的一种。最后一个选项是默认的,当函数调用被延迟,它可能不会运行了。

auto f5 = std::async(move_only());	//调用temp(),temp是通过std::move(move_only())构造得到

auto f6 = std::async(std::launch::async,Y(),1.2);//在新线程上运行
auto f7 = std::async(std::launch::deferred,barz,std::ref(x)); //在wait或者get调用时运行
auto f8 = std::async(std::launch::deferred|std::launch::async,
					 bar,std::ref(x));	//实现选择执行方式
auto f9 = std::async(bar,ref(x));
f7.wait();	//调用延迟函数

4.2.2任务与期望

std::pakaged_task<>对一个函数或可调用对象,绑定一个期望。当std::pakaged_task<>对象被调用,它会调用相关函数或可调用对象,将期望状态置为就绪,返回值也会被存储相关数据。

4.8 std::pakaged_task<>偏特化


template<>
class packaged_task<std::string(std::vector<char*>,int)>
{
public:
	template<typename Callable>
	explicit pakaged_task(Callable&& f);
	std::future<std::string> get_future();
	void operator()(std::vector<char*>,int);
}

线程间传递任务

很多图形框架需要特定的线程去更新界面,所以当一个线程需要更新界面时,它需要发出一条信息给正确的线程,让特定的线程做界面更新。std::pakeged_task提供了完成这种功能的一种方法,且不需要发送一条自定义信息给图形界面相关线程。

4.9使用std::pakaged_task执行一个图形界面线程

#include <queue>
#include <mutex>
#include <future>
#include <thread>
#include <utility>

std::mutex m;
std::deque<std::packaged_task<void()> > tasks;

bool gui_shutdown_message_received();
bool get_and_process_gui_message();

void gui_thread()	//1
{
	while(!gui_shutdown_message_received())	//2
	{
		get_and_process_gui_message();	//3
		std::packaged_task<void()> task;
		{
			std::lock_guard<std::mutex> lk(m);
			if(tasks.empty())	//4
				continue;
			task = std::move(tasks.front());	//5
			tasks.pop_front();
		}
		task();		//6
	}
}

std::thread gui_bg_thread(gui_thread);

template<typename Func>
std::future<void> post_task_for_gui_thread(Func f)
{
	std::packaged_task<void()> task(f);		//7
	std::future<void> res = task.get_future();//8
	std::lock_guard<std::mutex> lk(m);	//9
	tasks.push_back(std::move(task));	//10
	return res;
}

图形界面线程1循环直到接收到一条关闭图形界面的信息后关闭2,进行轮询界面消息处理3,当队列没有任务4,它将再次循环;除非它能在队列中提取到一个任务5,然后释放队列的锁,并执行任务6.这里期望与任务有关,当任务完成后,其状态被置位就绪。

当一个任务传入队列:提供的函数7可以打包好一个任务,可以通过这个任务8调用get_future成员函数来获取期望对象,并且在任务被推入列表9之前,期望将返回调用函数10。当需要知道线程打包执行任务时,像图形界面线程发送消息的代码,会等待期望改变状态,否则,则会丢弃这个期望。

4.2.3 使用std::promises

std::promise<T>提供了设定值的方式,这个类型和std::future对象关联。一对std::promise/std::future会为这种方式提供一个可行的机制;在期望上可以阻塞等待线程,同时,提供数据的线程可以使用组合中的承诺来对相关的值进行设置,以及将期望的状态置为就绪。

可以通过get_future()成员函数来获取一个给定的std::promise相关的std::future对象,就像与std::packaged_task相关。当承诺值已经设置完毕(使用set_value()成员函数),对应的期望的状态变为就绪,并且可用于检索已存储的值。当你在设置值之前销毁std::promise,将会存储异常。

清单4.10中,是单线程处理多接口的实现,例子中可以使用一对std::promise<bool>/std::future<bool>找到一块传出成功的数据块;与期望相关值只是一个简单的成功或失败的标识。对于传入包,与期望有关的数据就是数据包的有效负载。

清单4.10 使用承诺来解决单线程的多连接问题

#include <future>

void process_connections(connection_set& connections)
{
	while(!done(connections))		//1
	{
		for(connection_iterator connection = connections.begin(),
			end = connections.end();	//2
			connection != end; ++connection)
		{
			if(connection->has_incoming_data())	//3
			{
				data_packet data = connection->incoming();
				std::promise<payload_type>& p = 
					connection->get_promise(data.id);	//4
				p.set_value(data.payload);
			}
			
			if(connection->has_outgoing_data())	//5
			{
				outgoing_packet data = connection->
					top_of_outgoing_queue();
				connection->send(data.payload);
				data.promise.set_value(true);	//6
			}
		}
	}
}

4.2.4 为期望存储异常

std::promise提供了存储异常的功能,当你希望存入一个异常而非一个数值的时,你就需要调用set_exception(),
而非set_value()。
 

extern std::promise<double> some_promise;

try
{
	some_promise.set_value(calculate_value());
}
catch(...)
{
	some_promise.set_exception(std::current_exception());
}

这里使用了std::set_exception来检索异常,可以用std::copy_exception()作为一个替换方案,std::copy_exception
会直接存储一个新的异常,而不抛出。
some_promise.set_exception(std::copy_exception(std::logic_error("foo")));

另一种向期望存储异常的方式:在没有调用承诺上的任何设置函数前,或正在调用包装好的任务时,
销毁与std::promise或std::packaged_task相关的期望对象。将会存储一个与std::future_errc::
broken_promise错误相关的std::future_error异常。

4.2.5 多个线程的等待

当多个线程需要等待相同的事件的结果,你就要使用std::shared_future来替代std::future。std::future模型独享同步结果的所有权,并且通过调用get函数,一次性获取数据,这让并发就变得毫无意义——只有一个线程可以获取结果值,因为第一次get后,就没有值可以再获取了。

std::future是只可以移动的,所以其所有权可以在不同实例中互相传递,但是只有一个实例可以获取特定同步结果;而std::shared_future实例可以拷贝的,所以多个对象可以引用同一关联期望的结果。

在每一个std::shared_future独立的对象上的函数调用的结果不是同步的,所以为了在多线程访问一个独立对象,避免数据竞争,必须用锁来访问进行保护。优先的办法:为了替代只有一个拷贝对象的情况,可以让每一个线程拥有自己对应的拷贝对象。这样每个线程都通过自己拥有的std::shared_future对象获取结果,那么多个线程访问共享同步的结果是安全的。

std::shared_future的实例同步std::future实例的状态。当std::future对象没有与其他共享同步状态所有权,那么所有权必须使用std::move将所有权传递到std::shared_future。

std::promise<int> p;
std::future<int> f(p.get_future());
assert(f.valid());	//1期望f时合法的
std::shared_future<int> sf(std::move(f));
assert(!f.valid());	//2期望f现在不合法
assert(sf.valid());	//3 sf现在是合法的

std::future有一个share()函数,可以用来创建新的std::shared_future,并且直接转移期望的所有权。

std::promise<std::map<SomeIndexType,SomeDataType,
SomeComparator,SomeAllocator>::iterator> p;
auto sf = p.get_future().share();

 

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值