以下内容为本人的学习笔记,如需要转载,请将本段内容(无删改)张贴于文章顶部:微信公众号「ENG八戒」mp.weixin.qq.com/s/vCKVYZYdq…,更多无限制精彩内容欢迎查阅我的个人博客站点 ENG八戒
话说上文从线程聊到 std::promise 和 std::future,今天继续聊异步编程。
std::packaged_task
在应用 std::promise 和 std::future 快速实现跨线程异步计算的实例代码里,其实有一个很不爽快的问题,任务函数的计算结果没有直观地通过 return 语句返回。
由于 std::thread 不能接受线程函数的返回值,导致需要将 std::promise 对象传递进入线程执行函数(任务函数)中,再利用这个 std::promise 对象传递计算结果给调用线程的 std::future 对象。
虽然目的是转存计算结果,但是任务函数变得冗余。简直就像,让你发现显示屏上多了根头发,怎么看怎么难受!
既然不喜欢这样的写法,那么我们就想办法优化。
任务函数的计算结果应该是直观地通过函数 return 语句返回:
int compute(int a, int b) {
int sum = a + b;
// 模拟耗时
std::this_thread::sleep_for(std::chrono::seconds(1));
return sum;
}
在创建 std::thread 对象时,原应传递的任务函数指针改为传递一个函数对象,其余参数照旧。这个函数对象应该内部调用了任务函数,并接收返回值,再通过 std::promise 传递到调用线程的 std::future 对象。
为了创建这个函数对象,可以封装一个类 my_packaged_task。这个类 my_packaged_task 也应该提供方法 get_future() 供调用线程获取与 std::promise 对象关联的 std::future 对象。
基于这种思路,原先的启动异步计算的过程可以如下:
int main() {
my_packaged_task<int(int, int)> task(compute);
std::future<int> f = task.get_future();
std::thread t(std::move(task), 3, 4);
t.detach();
std::cout << "get compute result:"
<< f.get() << std::endl;
return 0;
}
这样,只要实现了类 my_packaged_task,std::promise 就可以被隐藏起来了,再也不用关心它到底如何赋值,在哪里赋值。
为了适应任务函数的多样性,包括返回值的类型、型参的类型等,my_packaged_task 应该被定义为模版类。
template <typename Func>
class my_packaged_task;
template <typename Ret, typename... Args>
class my_packaged_task<Ret(Args...)> {
//...
};
先声明 my_packaged_task 的通用模板,再定义模板的特例化。Ret 代表任务函数的返回值类型,Args 代表任务函数的可变长参数的类型。
判断模版定义是不是特例化,可以依据模板类型后边是否有
<>来决定。
实现 my_packaged_task 模板的特例化时,需要定义被隐藏的 std::promise 对象成员:
template <typename Ret, typename... Args>
class my_packaged_task<Ret(Args...)> {
private:
std::promise<Ret> promise_;
//...
};
为了能调用任务函数,也需要定义一个可调用对象成员:
template <typename Ret, typename... Args>
class my_packaged_task<Ret(Args...)> {
private:
//...
std::function<Ret(Args...)> func_;
public:
my_packaged_task(std::function<Ret(Args...)> func)
: func_(std::move(func)) {}
//...
};
如上,任务函数可以在实例化 my_packaged_task 类时指定。
std::thread 对象在启动线程执行函数(任务函数)时,会调用 my_packaged_task 函数对象的 () 操作符。所以可以在重写 my_packaged_task 类的 () 操作符时,调用任务函数,然后获得任务函数的返回值,再调用 std::promise 对象赋值。
template <typename Ret, typename... Args>
class my_packaged_task<Ret(Args...)> {
//...
public:
//...
void operator()(Args&&... args) {
try {
promise_.set_value(func_(std::forward<Args&&>(args)...));
} catch (...) {
promise_.set_exception(std::current_exception());
}
}
//...
};
与 std::promise 对象关联的 std::future 对象需要通过成员 get_future() 返回:
template <typename Ret, typename... Args>
class my_packaged_task<Ret(Args...)> {
//...
public:
//...
std::future<Ret> get_future() {
return promise_.get_future();
}
};
从以上的优化思路来看,my_packaged_task 就是一个任务函数和 std::thread 的适配器。
是的,在以上的过程中,我们实现了标准库里 std::packaged_task 的简化版。如果在 main 函数里直接把 my_packaged_task 替换成 std::packaged_task,可以直接编译,并且运行结果是一致的,所以我们可以直接使用 std::packaged_task 来简化任务函数的结果返回。
std::async
到目前为止,为了启动异步计算的过程还是需要创建一些和计算过程无关的对象,比如 std::packaged_task 和 std::thread 对象等,而真正有意义的就只有用于获取异步计算结果的 std::future 对象。
所以,我们可以大胆地设想一下,能不能继续优化,比如用一个函数或类对象就把 std::packaged_task 和 std::thread 对象等封装起来,对于用户来讲,无须关心它们,仅需要通过这个函数或者类获取 std::future 对象即可。
假设我们用函数 my_async 实现了上面的猜想,启动异步计算的过程就可简化成这样:
int main() {
std::future<int> f = my_async(compute, 3, 4);
std::cout << "get compute result:"
<< f.get() << std::endl;
return 0;
}
任务函数 compute 和前一个例子无异,已经是非常干净俐落的了,不打算再深究它。那么这个 my_async 到底该如何实现?
从最终简化的目标来看,启动异步计算的过程省略了这部份的代码:
std::packaged_task<int(int, int)> task(compute);
std::future<int> f = task.get_future();
std::thread t(std::move(task), 3, 4);
t.detach();
那么是否可以直接把这段代码封装起来?应该是可以的,但是基于 compute 的形式是可变化的,比如上面的这段代码里 compute 的类型为 int(int, int),如果换成其它类型又如何?可见 my_async 应该是用模版函数实现:
template <typename Func, typename... Args>
std::future<typename std::result_of<Func(Args...)>::type>
my_async(Func&& func, Args&&... args) {
using Result_t = typename std::result_of<Func(Args...)>::type;
std::packaged_task<Result_t(Args...)> task(func);
std::future<Result_t> future = task.get_future();
std::thread t(std::move(task), args...);
t.detach();
return future;
}
这里为什么必须要用 t.detach() 呢?是否可换成 t.join()?
my_async 的目标是启动异步计算,那么调用 my_async 的时候我们假设也是不能阻塞的,所以线程应该被放飞而不是等待线程函数执行返回。
事实上,标准库也提供了 std::async() 模版函数实现以上的优化,但是 std::async() 实现的功能更完善,不仅仅是提供立刻执行的异步处理,还提供了延迟的同步处理。
std::async() 提供的立刻执行的异步处理就和上面我们实现的 my_async 功能一样,使用形式还需要添加指明使用的策略 std::launch。
策略有三个:
-
std::launch::async,保证行为是异步的,任务函数是在子线程中被执行。
-
std::launch::deferred,不是异步行为,任务函数会被延迟执行,比如调用
std::future::get()或std::future::wait()时,才触发执行,而且不会创建新线程。总结起来,就是需要结果时才执行计算。 -
默认值,类似于 std::launch::async | std::launch::deferred,是否是异步执行,依赖于系统负载,用户无法控制。
更多
比较新的 C++ 20 还提供了 std::jthread 和协程(coroutine),边幅有限,暂不继续展开了,有空再聊这个话题。