c++26新功能——std::execution
一、异步编程
不管什么语言的开发者,大家都知道,异步编程的难度是相当高的。那么到底有多高呢?对于一些天然支持异步操作的语文或库(框架等)来说,能够用好本身就是一个比较复杂的情况。在前面也分析过,异步编程涉及到编程的思想的变化和设计框架的不同。它不仅仅是一个简单的同步变异步的过程,这也是人们常说的思想不过关,早晚出问题的一个实例。
更何况C++这种天然不支持异步编程的语言,想到实现异步编程,就必须手动从头搞一搞。多线程尚且让大多数程序员在头痛,如果再加上异步,估计头就不会只是痛了,可能直接就要爆掉了。
虽然在C++11后提供了一些基础的异步编程的接口或库,但它太简单了,应用上也并不普及,对异步编程(如std::async)也只是涉及到了很浅显的部分。
二、std::execution
早就传说异步执行库要加入新的C++标准,但这个加入的过程可谓道阻且长,但在C++26中,基本定了下来。std::execution,执行控制库提供了在通用资源上执行异步任务的框架。
不过这个库目前应该是还没真正完善,只提供了一些简单、基础的方式来构建任务执行图。开发者基本都知道,异步大多数情况是用线程(复杂就是线程池甚至并行库)实现的,协程出现后可能会使用协程实现,所以稍微复杂的异步执行可以理解为任务的图构建执行的过程。
看一下具体的定义:
1、Sender: A description of asynchronous work to be sent for execution. Produces an operation state (below).
Senders asynchronously “send” their results to listeners called “receivers” (below).
Senders can be composed into task graphs using generic algorithms.
Sender factories and adaptors are generic algorithms that capture common async patterns in objects satisfying the sender concept.
2、Receiver: A generalized callback that consumes or “receives” the asynchronous results produced by a sender.
Receivers have three different “channels” through which a sender may propagate results: success, failure, and canceled, so-named “value”, “error”, and “stopped”.
Receivers provide an extensible execution environment: a set of key/value pairs that the consumer can use to parameterize the asynchronous operation.
3、Operation State: An object that contains the state needed by the asynchronous operation.
A sender and receiver are connected when passed to the std::execution::connect function.
The result of connecting a sender and a receiver is an operation state.
Work is not enqueued for execution until “start” is called on an operation state.
Once started, the operation state’s lifetime cannot end before the async operation is complete, and its address must be stable.
4、Scheduler: A lightweight handle to an execution context.
An execution context is a source of asynchronous execution such as a thread pool or a GPU stream.
A scheduler is a factory for a sender that completes its receiver from a thread of execution owned by the execution context.
其实学习过一些任务图编程或并行编程(如前面的TBB等),就会明白,所谓的线程任务图执行,其实涉及到任务的制定分配、任务的执行、当前的任务状态和任务的调度以及任务执行结果。而这其中任务的执行一般会被底层框架和机器(CPU或GPU执行),开发者能看到的基本就是任务的制定分配(上面的Sender),任务的执行结果(Receiver),任务的状态(Operation State)以及任务调度器(Scheduler)。
相信亲自手写过线程池的人都会非常熟悉这种编程方式(初次接触的需要认真思考一下)。那么此时再回头看上面的英文,应该就比较清楚了。
上面的英文描述也写得很清楚,利用到了回调函数、线程池和任务队列(图),还是那句话,如果有相关的线程池或异步编程的经验,理解和学习std::execution应该没有什么困难。
三、相关的支持
在C++26中,开发者可以看说明中是有点(Point和Node)这个名词的,但目前没有更详细的说明,可能最终的版本仍然未完全确定,所以还不敢写到文档中。有兴趣且英文不错的可以翻翻一下相关的技术草案(https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2024/p2300r10.html)。
特别是execution::schedule隐约可以看到TBB中节点连接的味道,只要是异步大约就会和图搞到一起,而节点间的控制就是一个首要的问题。更具体的一些细节,参看上述的草案地址。
四、代码示例
看一下简单的代码说明:
#include <cstdio>
#include <execution>
#include <string>
#include <thread>
#include <utility>
using namespace std::literals;int main()
{std::execution::run_loop loop;std::jthread worker([&](std::stop_token st){std::stop_callback cb{st, [&]{ loop.finish(); }};loop.run();});std::execution::sender auto hello = std::execution::just("hello world"s);std::execution::sender auto print= std::move(hello)| std::execution::then([](std::string msg){return std::puts(msg.c_str());});std::execution::scheduler auto io_thread = loop.get_scheduler();std::execution::sender auto work = std::execution::on(io_thread, std::move(print));auto [result] = std::this_thread::sync_wait(std::move(work)).value();return result;
}
还有一个草案上的代码:
using namespace std::execution;scheduler auto sch = thread_pool.scheduler(); // 1sender auto begin = schedule(sch); // 2
sender auto hi = then(begin, []{ // 3std::cout << "Hello world! Have an int."; // 3return 13; // 3
}); // 3
sender auto add_42 = then(hi, [](int arg) { return arg + 42; }); // 4auto [i] = this_thread::sync_wait(add_42).value(); // 5
代码很简单,对照着上面的说明,会很容易理解代码本身及上面的英文说明。当然,这涉及到一些jthread的相关技术,如果没接触过的,可以看看,或者简单理解为安全线程即可。
五、总结
本文是对c++26中的std::execution库的一个入门的分析介绍,目前支持其的平台或编译器尚且在进行中,所以大家可以找到一些编译器尝鲜试试。在翻看一些国外的网站时,发现有的公司已经在生产中使用这个库了。国内的开发者,还要努力跟进啊。