当线程需要等待一个数据的有效性时,condition_variable就显得不是很适合了,此时应该使用future。
想象你将要坐飞机去休假,办完手续后你需要几个小时去等待飞机起飞。此时你会选择做一些事情来打发时间,比如喝咖啡、看书等等,但根本上讲你只为等一件事:登机。而且,你本次等的航班是唯一的。下次你再旅行就要等待另一趟航班。
C++标准库将这种一次性事件称作一个future。如果一个线程需要等待一个一次性事件,那么它将通过某种方式获取到一个代表这个事件的future。然后,以小周期来检查这个事件是否发生,并在检查间隙做一些其他事情。或者,在需要这个事件发生之前线程一直做其他事情,直到没有事做了,再一直等待这个事件。一个future可能有与之关联的数据,也可能没有。一旦事件发生(future的状态变为ready),则future的状态不可以被重新设置。
在
模板的类型参数就是future对象关联的数据类型,std:future
最基本的一次性事件来自计算的结果,这个结果是由后台计算得到的。回想第2章,一个thread对象没有提供任何获取返回值的手段,future可以达到这个目的。
从后台任务返回一个值
想象一下,一个需要很长时间的计算你期待他将要计算出结果,但是你现在却不需要它。你可能想到一种方法:使用一个新线程去处理这个计算结果,这意味着你不得不小心的将结果传回来,因为thread对象并没有为此提供任何手段。这就是 std::async函数模板的用处。
我们使用 std::async来开启一个异步任务,这个任务的结果不是立刻需要的。他并不是给你一个thread对象,让你去join,相反,他给你返回一个future对象,这个对象将保持着任务的结果。当你需要这个结果时,你仅仅需要调用future对象的get()函数,线程将阻塞直到future对象的状态变为ready并返回结果。看下面的示例:
int find_the_answer_to_ltuae();
void do_other_stuff();
int main()
{
std::future the_answer = std::async(find_the_answer_to_ltuae);
do_other_stuff();
std::cout << "The answer is " << the_answer.get() << std::endl;
}
std::async()的调用方法与thread的构造函数一样,如果第一个参数是一个类的成员指针,则第二个参数要指出哪个对象来执行(要么是拷贝,要么是传指针,要么使用std::ref()),剩下的参数就是传给成员函数的了。否则,第二个参数及之后的参数都是传给第一个函数或者可调用对象的。不管是函数对象还是参数都允许传入右值,这是为了允许使用一些只能move的对象。例如:
struct Y
{
double operator()(double);
};
Y y;
auto f3=std::async(Y(),3.141);//使用Y的移动构造函数
X baz(X&);
std::async(baz,std::ref(x));//使用x的引用
class move_only
{
public:
move_only();
move_only(move_only&&)
move_only(move_only const&) = delete;
move_only& operator=(move_only&&);
move_only& operator=(move_only const&) = delete;
void operator()();
};
auto f5 = std::async(move_only());//使用移动构造
async还有另一个重载版本:需要将第一个参数指定为std::launch,这是一种enum类型,用于指定async函数执行的方式。
std::launch::deferred用于指定函数同步执行,并不开启新线程,只有当其返回的future调用get()或wait()函数时才会去执行。也就是说如果一直不调用get()或wait(),则一直不执行。
std::launch::async用于指定函数异步执行,开启新线程。
如果不指定这个参数,那么具体如何执行将由系统决定。
get()直接返回future关联的计算结果的拷贝。wait()用于不需要得到数据结果时,仅仅是为了得到一个信号。
对于std::future来说,只能调用一次get(),多次调用会导致异常。
另外,还有wait_for()、wait_utill()函数,提供有限时间的等待。
使用future关联一个任务
std::async()函数并不是将future与任务关联的唯一手段,也可以通过将任务封装到一个 std::packaged_task<>对象中,或者通过 std::promise<>显示的设置值来达到这个目的。std::packaged_task<>是比std::promise<>更高一级的抽象,因此,我们先从std::packaged_task<>说起。
std::packaged_task<>用于连接一个future和一个函数(或可调用对象)。packaged_task类重载了函数调用运算符,当一个std::packaged_task对象被调用的时候,他将调用其保存的函数或可调用对象,并返回一个保存了调用结果的future对象,这个对象的状态是ready。这种机制可以用于线程池的构件块(第9章),或者作为其他的任务管理机制,例如让任务运行在他自己的线程中,或者让他们在一个后台线程中顺序执行。
如果一个大型的操作可以被分割成多个独立的小任务,它们中的每一个都可以被包装到一个packaged_task实例中,然后这些实例被发送给任务调度程序,或者是发给线程池。这相当于将任务从细节中抽象出来,任务调度只需要调用这些packaged_task,而不是一个个的函数。
它的模板参数是一个函数声明,比如: int(std::string&,double*)代表一个带有2个参数并且返回int的函数声明。当你构造一个packaged_task对象时,你必须传递一个函数或者可调用对象作为参数,传递的函数或者可调用对象的调用形式不比与模板参数中的函数声明严格一致,只要可以通过隐式转换打到调用目的即可。通过packaged_tast的函数get_future()可以得到保存结果的future对象,这个future对象的模板类型由packaged_task的模板参数指定的函数声明的返回值决定,而packaged_tast的调用方式则由函数声明中的参数类型来决定。例如一个 std::packaged_task
template<>
class packaged_task*, int)>
{
public:
template
explicit packaged_task(Callable&& f);
std::future get_future();
void operator()(std::vector*, int);
};
一个std::packaged_task对象就是一个可调用对象,它可以被包装到一个std::funtion对象中,或者作为std::thread对象的构造参数,或者传给另一个接受可调用对象的函数,或者直接被调用。创建一个packaged_task后,需要先调用get_future获取future对象,然后再将packaged_task对象传给别的地方,顺序不可弄反。当你想要获取任务结果的时候,你可以调用future的wait或get函数。
注意,std::packaged_task对象只能移动,不可以拷贝,因此一旦传走,那么原来的对象就再也不能直接使用了。
在线程中传递任务
许多GUI框架的GUI更新都是通过一个指定的线程来实现的,所以其他线程如果想更新界面必须给指定的线程发送信号。std:packaged_task提供一种方法,可以不用定义自定义消息,如下:
std::mutex m;
std::deque > tasks;
bool gui_shutdown_message_received();
void get_and_process_gui_message();
void gui_thread()
{
while (!gui_shutdown_message_received())
{
get_and_process_gui_message();
std::packaged_task task;
{
std::lock_guard lk(m);
if (tasks.empty())
continue;
task = std::move(tasks.front());
tasks.pop_front();
}
task();
}
}
std::thread gui_bg_thread(gui_thread);
template
std::future post_task_for_gui_thread(Func f)
{
std::packaged_task task(f);
std::future res = task.get_future();
std::lock_guard lk(m);
tasks.push_back(std::move(task));
return res;
}
post_tsk_for_gui_thread()函数负责将函数void()类型的函数放入队列中,将得到的future作为返回值。调用这个函数后,得到的future可以用来获取返回值,如果不想获取也可以直接丢弃,但不论是否使用future的get或者wait函数,它对应的任务都会被gui_thread()函数执行。