Modern C++ Concurrency Utilities 06: Promise, Future and Asynchronous
「期值」是标准库为异步任务中返回值以及异常的传递提供的基础设施. 具体的做法是将这些值关联到共享的「future」对象上. 异步任务可以向这些期值对象中写入返回值或异常值, 异步任务的发起线程则可以通过这些期值对象等待, 读取, 检查其所需要的异步结果. 标准库中为上述功能提供的设施即 std::promise 和 std::future.
std::promise
promise 为异步任务提供了存储返回值或异常的机制, 可以说, 在「期值」构建的异步交互机制中, promise 对象及其接口负责 写入. 异步任务所存储的值可以被其他线程异步地通过该 promise 对象构建出的 std::future 对象获取.
从语义上看, 每个 std::promise 对象都表示一个「共享的状态」, 并且这个共享的状态可能还没有被计算和设置, 或者在计算时发生了异常. 异步程序可以对 std::promise 对象执行三种操作以设置其所代表的共享状态的状态:
- make ready: 将该
promise对象所代表的共享状态设置为有效. 唤醒其他正在等待该共享状态的线程以读取该状态. - release: 将该
promise对象和其关联的共享状态断开关联. - abandon: 设置该
promise对象所代表的共享状态为异常状态, 然后 make ready 并 release
std::promise 提供的以下方法对应于上述三种抽象操作:
-
set_value: 相当于 make ready, 向promise对象所代表的共享状态设置值并标记为 ready. -
set_exception: 相当于 abandon, 将promise对象所代表的共享状态设置为异常状态并 make ready需要注意一个
promise对象只能set_*一次, 即如果已经调用了set_value或set_exception, 那么新的set_*操作会抛出异常. -
get_future: 获取一个与当前promise对象关联的future对象, 相当于获取当前promise对象关联的共享状态的异步读取接口.需要注意的是一个
promise只能get_future一次, 如果多次get_future会抛出异常. -
set_value_at_thread_exit: 与set_value不同之处在于该函数会设置promise所代表的共享状态的值, 但是不会立即 make ready, 而是在当前线程退出时才 make ready -
set_exception_at_thread_exit: 与set_value_at_thread_exit同理
std::future
future 提供了「期值」体系中的异步读取接口. 对于通过 std::async, std::packaged_task 等工具创建的异步任务, 或者手动使用 std::promise 的异步任务, 都提供了获取 future 对象的接口, 异步任务的创建者可以通过该接口获取 future 对象并读取结果, 或者在结果尚未计算完成并设置时等待结果, 或者在异步任务异常时捕获异常.
future 提供以下方法实现对异步结果的等待/验证/读取:
get: 阻塞地等待future对象所关联的共享状态被设置有效的值, 并且获取该值.valid: 检查future对象所关联的共享状态是否有效. 所谓「有效」指future对象与一个共享状态相关联. 如果直接默认构造一个future对象而不是从async/packaged_task/promise获取future对象, 则valid()返回false. 当对一个有效的future对象调用get后,valid也返回false.wait: 阻塞等待到future对象所关联的共享状态可被读取.wait_for/wait_until:wait函数的 timeout 版本share: 将当前future对象关联的共享状态 转移 到一个std::shared_future对象, 并返回该std::shared_future对象.
std::shared_future
std::future 独占地指涉到共享状态 (不可拷贝), 不允许多个线程持有对一个共享状态的指涉, 因而无法做到多个线程访问该共享状态. std::shared_future 则可以做到多个 std::shared_future 对象指涉同一个共享状态 (可以拷贝). 同时多个线程通过不同的 std::shared_future 对象并发访问同一个共享状态是安全的.
std::shared_future 提供的方法在语义上只和 std::future 存在一点差别: 调用 get 后不再导致 valid 返回 false.
std::async
std::async 函数模板接受可调用对象及其参数作为参数, 它会根据传入的执行策略参数 异步 地执行传入的可调用对象. std::async 会立即返回一个 std::future 对象, 作为读取传入的异步任务返回值的接口.
其中「执行策略」是一个枚举, 即 std::launch. 标准定义了两种执行策略:
std::launch::async: 异步执行传入的可调用对象, as if 在一个新的std::thread对象中执行std::launch::deferred: 将传入的可调用对象和其参数存储在返回的future对象所指涉的共享状态中. 将其执行推迟到第一次对返回的future对象调用 非 timeout 版本的wait函数时. 后续对该future对象的访问 (如get) 将立即返回结果.
std::packaged_task
std::package_task 类模板提供一个 wrapper, 可以将任意的可调用对象 wrap 成异步任务 (可异步调用的可调用对象). 被 wrap 的异步任务的返回值或异常通过 future 对象传递. std::package_task 可以看作是 std::function 的异步版本.
valid: 检查当前packaged_task是否持有一个共享状态.get_future: 返回一个指涉到当前packaged_task所持有的共享状态的future对象.operator(): 调用其 wrap 的可调用对象, 返回值或异常将被储存到其持有的共享状态中.std::make_ready_at_thread_exit: 执行其 wrap 的可调用对象, 并将返回值或异常储存到共享状态中, 但是并不立即将共享状态 make ready, 而是在当前线程退出时才 make ready.reset: 重置内部状态, 抛弃之前的执行结果并在内部重建新的共享状态.