期望(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();