Introduction
编程难,多线程编程更难 – 鲁迅
多线程编程,如此令人着迷、令人痛恨的字眼。人类为了追求更好的效率、更快的速度,非常残忍的发明了多线程编程,这不仅让写代码的难度陡增,同时也加快了头发掉落的速度,写到这时,我不禁感觉头上又凉了一些。
最近,我一脚踏进了 C++ 多线程编程的海洋中,在各种 std::thread
、std::condition_variable
、std::mutux
、std::unique_lock
、std::async
等等中挣扎徘徊。并且,同事告诉我,这些小玩意只能算是餐前开胃菜,后面还有更棒棒的东西在等我,比如内存模型、无锁编程等等。听到着,我紧张地打开我的保温杯,多抓了一把枸杞放了进去。
饭要一点一点的吃,知识要一点一点的学。在学习过程中,我对 async、packated_task、promise 这仨的区别、以及应该在什么场景使用它们,都相当的困惑。在查阅了一些资料后,我按照个人理解总结了这篇文章。希望能够帮助到一些和我有一样疑问的小朋友们。当然,作为一名菜鸡,文章不可以避免会出现错误,也希望各位大佬指正。
使用方法
std::async
、std::packaged_task
和 std::promise
们都被安排在 《C++ Concurrency In Action》的 4.2 章,它们都能拿到一个 std::future
。这些相似的地方是最初让我困惑地方,它们似乎有所区别,但是一开口却又具体说不上来。
我们先回顾下 std::async
、std::packaged_task
和 std::promise
的基本使用。首先从最直观的使用方式上来感受它们的差别。
std::async
当不着急要任务结果的时候,可以使用 std::async
启动一个异步任务,std::async
返回一个 std::future
对象,std::future
对象中存放着最终计算的结果。
一切都这么简单,这是获取 std::future
最简洁的方式。
当需要最终结果时,调用 std::future::get()
方法即可,该方法会阻塞线程直到期望值状态就绪为止。下面的代码是个简单例子:
#include <future>
#include <iostream>
int find_the_answer_to_ltuae();
void do_other_stuff();
int main()
{
std::future<int> the_answer=std::async(find_the_answer_to_ltuae);
do_other_stuff();
std::cout<<"The answer is "<<the_answer.get()<<std::endl;
}
std::async
并不总会开启新的线程来执行任务,你可以指定 std::launch::async
来强制开启新线程
auto f = std::async(std::launch::async, func);
值得注意的是,std::future
析构函数会阻塞,直到线程结束。通常我们认为 std::future::get()
和 std::future::wait()
才会阻塞,析构函数同样也会,这一点需要特别小心。
auto sleep = [](int s) { std::this_thread::sleep_for(std::chrono::seconds(s)); };
{
auto f = std::async( std::launch::async, sleep, 5 ); // 开启一个异步任务,睡眠 5s
// future 对象析构,等待睡眠结束
}
如果 std::future
被存放在一个临时对象中,那么std::async
会立马阻塞,因为临时对象在返回后立马被析构了。例如下面的代码中将会阻塞 10s,但是如果加上 auto f =
那么只会阻塞 5s
auto sleep = [](int s) { std::this_thread::sleep_for(std::chrono::seconds(s)); };
{
std::async( std::launch::async, sleep, 5 ); // 临时对象被析构,阻塞 5s
std::async( std::launch::async, sleep, 5 ); // 临时对象被析构,阻塞 5s
//auto f1 = std::async( std::launch::async, sleep, 5 );
//auto f2 = std::async( std::launch::async, sleep, 5 );
}
std::packaged_task
std::packaged_task
本身和线程没啥关系,它只是一个关联了 std::future
的仿函数。看下面这个例子:
auto task = [](int i) {
std::this_thread::sleep_for(std::chrono::seconds(5)); return i+100;
};
std::packaged_task< int(int) > package{ task };
std::future<int> f = package.get_future();
package(1);
std::cout << f.get() << "\n";
我们调用 package(1)
就开始执行函数,就像执行了 task(1)
一样。运行结束后,f
已经处在就绪转态,因此 .get()
并不会阻塞。
因为 std::packaged_task
是个仿函数,我们用它来创建 std::thread
,并通过 std::future
来获取运行的结果,看这个例子:
std::packaged_task< int(int) > package{ task };
std::future<int> f = package.get_future();
std::thread t { std::move(package), 5 };
std::cout << f.get() << std::endl; // 阻塞,直到线程 t 结束
t.join();
可以看到,std:packaged_task
的使用稍微麻烦一些,需要显式的调用或者传递给std::thread
进行异步调用,但其具有更加灵活的控制调用方式,并且可以选择什么时间开始任务,而 std::async
则是一旦调用立马开始执行,并且直接调用 std::async()
中临时变量析构的导致阻塞的坑,std::packaged_task
没有。
需要注意的是,一定过一定要在调用 f.get()
之前,执行了 std::packaged_task
,否则你的程序会一直阻塞在那:
std::packaged_task<int(int,int)> task(...);
auto f = task.get_future();
std::cout << f.get() << "\n"; // oops!
task(2,3);
std::promise
std::promise
是一种非常强大的机制。例如,你可以将一个值传递给新进程,而不需要任何额外的同步操作。
auto task = [](std::future<int> i) {
std::cout << i.get() << std::flush; // 阻塞,直到 p.set_value() 被调用
};
std::promise<int> p;
std::thread t{ task, p.get_future() };
std::this_thread::sleep_for(std::chrono::seconds(5));
p.set_value(5);
t.join();
自顶向下
介绍完如何使用,现在我们来思考 std::async
、std::packaged_task
和 std::promise
之间的关系。总体来说,std::async
接口最简单,做的事情最多,抽象程度最高;std::packaged_task
,抽象程度次之,需要额外的操作但却比较灵活;std::promise
功能最为单一,是三者中抽象程度最低的。
我们用 std::packaged_task
来实现 std::async
的功能:
std::future<int> my_async(function<int(int i)> task, int i)
{
std::packaged_task<int(int)> package{task};
std::future<int> f = package.get_future();
std::thread t(std::move(package), i);
t.detach();
return f;
}
int main()
{
auto task = [](int i) { std::this_thread::sleep_for(std::chrono::seconds(5)); return i+100; };
std::future<int> f = my_async(task, 5);
std::cout << f.get() << std::endl;
return 0;
}
在 0 处通过 std::packaged_task::get_future
获得一个 std::future
用于返回;1 处我们将std::packaged_task
用新线程启动,等于 std::async
中使用了 std::launch::async
参数; 在 2 处,无需等待新线程运行结束,因此直接.detach()
线程。可以看到,std::packaged_task
完全可以实现 std::async
的全部功能,并且返回的 std::future
并不会在析构的时候阻塞。
接下来我们用 std::promise
来实现 std::packaged_task
。
template <typename> class my_task;
template <typename R, typename ...Args>
class my_task<R(Args...)>
{
std::function<R(Args...)> fn;
std::promise<R> pr; // the promise of the result
public:
template <typename ...Ts>
explicit my_task(Ts &&... ts) : fn(std::forward<Ts>(ts)...) { }
template <typename ...Ts>
void operator()(Ts &&... ts)
{
pr.set_value(fn(std::forward<Ts>(ts)...)); // fulfill the promise
}
std::future<R> get_future() { return pr.get_future(); }
// disable copy, default move
};
这个简易版的 my_task
和 std::packaged_task
使用上没啥区别,内部用了 std::promise
来获取 std::future
,在 operator()
中通过 std_value()
方法来传递数据,让 std::future
处于就绪状态。
总结
通过上面的描述,你应该对如何使用,以及它们之间的层级关系有一定了解了。总结一下:
- 用
std::async
来做简单的事情,例如异步执行一个任务。但是要注意std::future
析构阻塞的问题。 std::packaged_task
能够很轻松的拿到std::future
,选择是否配合std::thread
进行异步处理。同时没有析构阻塞的问题。std::promise
是三者中最底层的能力,可以用来同步不同线程之间的消息