Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

任务组合

在实际的工程项目中,各种任务通常以多种逻辑关系和时序关系组合在一起,而不是单纯的等待任务完成后再进行下一个任务, ASCO 提供了一些任务组合方式。

另外,除 select<N> 外,大多数组合函数都支持在同步上下文中调用。


非阻塞的串行任务组合 future::then()

future<T>::then() 用于将一个异步操作的结果传递给下一个异步操作,实现异步任务的串行组合。 它允许你在前一个 future 完成后,自动将结果传递给指定的异步函数,并返回一个新的 future,表示整个组合任务的最终结果。

同时它返回另一个 futur ,可以继续对它进行任意的任务组合操作。

用法

future<int> f1 = some_async_func();
auto f2 = std::move(f1).then([](int result) -> future<std::string> {
    // 可以使用 result 进行后续异步操作
    co_return std::to_string(result);
});

then() 接收一个右值引用的 deduced this,通常在异步函数调用后直接调用 then() 即可,这里为了表示返回值类型, 特意先构造一个 future 对象,再将它移动。

传入的参数如果是 lambda 表达式,其参数不可以使用 auto 自动推导。

行为说明

  • then() 会等待当前 future 完成,将其结果作为参数传递给 f
  • then() 返回的 future 表示整个链式异步操作的最终结果。
  • 支持异常传递,若前一个 future 抛出异常,则异常会传递到 then 返回的 future。

注意事项

  • then() 只支持异步函数(返回 future)的链式组合。
  • 若在 future_inline 上调用 then,会自动将任务迁移到当前 worker 上执行。

异步的异常捕获 future::exceptionally()

future<T>::exceptionally() 用于为异步任务链提供异常处理能力。当前一个 future 抛出指定的异常时, exceptionally 可以捕获并执行用户自定义的异常处理逻辑,如果抛出的不是所指定的异常,则继续重抛至 exceptionally 自己的 future。

用法

future<int> f = some_async_func();
auto f2 = std::move(f).exceptionally([](const std::runtime_error& e) {
    std::cout << "caught: " << e.what() << std::endl;
});

exceptionally() 接收一个右值引用的 deduced this,通常在异步函数调用后直接调用即可。 传入的参数可以是 lambda 表达式或其它可调用对象,参数类型需为异常类型或异常类型的引用或 std::exception_ptr

行为说明

  • exceptionally() 会等待当前 future 完成 。
  • 如果当前 future 正常返回,则 exceptionally 返回的 future 也正常返回原值。
  • 如果当前 future 抛出异常,且异常类型与处理函数参数类型匹配,则调用处理函数,并返回 std::unexpected 包装的错误类型。
  • 如果异常类型不匹配,则异常会继续向上传递。
  • 返回类型为 future<std::expected<T, E>>,其中 E 由处理函数的返回类型自动推断。

注意事项

  • 处理函数参数类型必须能匹配 future 抛出的异常类型,否则异常不会被捕获。
  • 处理函数可以为 lambda、函数指针、std::function 等。
  • 处理函数返回类型可以为 void,此时异常类型为 std::monostate,否则为实际返回类型。
  • 若需捕获所有异常,可使用 std::exceptionstd::exception_ptr 作为参数类型。

协程打断异常

当协程被打断(如通过 future.abort()select<N> 选择逻辑打断其它分支)时,应抛出 coroutine_abort 异常。 你可以通过以下两种方式处理协程打断:

  1. 手动 try-catch 捕获

    在协程体内用 try { ... } catch (const coroutine_abort&) { ... } 捕获打断异常,实现自定义清理或日志逻辑。

  2. 使用 future.aborted() 统一处理

    通过 future<T>::aborted() 方法,可以为打断异常单独指定处理逻辑。例如:

auto f = some_async_func().aborted([] {
    std::cout << "协程被打断" << std::endl;
});

当协程被打断时,aborted() 传入的处理函数会被调用,并返回 std::nullopt,否则正常返回结果。


竞态任务组合(选择逻辑) select<N>

选择最先返回的协程继续运行,打断其它未返回或后返回的协程。

asco::interval in1s{1s};
asco::interval in500ms{500ms};
for (int i{0}; i < 6; i++) {
    switch (co_await asco::select<2>{}) {
    case 0: {
        co_await in1s.tick();
        std::cout << "1s\n";
        break;
    }
    case 1: {
        co_await in500ms.tick();
        std::cout << "500ms\n";
        break;
    }
    }
}

选择器将当前协程克隆出 N 个协程并同时唤醒运行。对构造的 select<N> 对象 co_await 后按协程被克隆的顺序返回 size_t 类型的值。

选择器仅对 select<N> 对象返回后的第一个异步任务有效。

最先返回的异步任务会将其它任务打断,因此即使后来的协程的已经返回,也会根据前文规定的可打断特性将其影响的状态恢复。

被打断的协程会将其调用者一并销毁;如果正确使用了 select<N> ,其调用者总是被克隆的 N 个协程中的 N-1 个。