C++并发之协程(Coroutines,co_await,co_yield, co_return)

协程(Coroutines)

  协程(Coroutines )是一个可以挂起执行以便稍后恢复的函数。协程是无堆栈的:它们通过返回到调用方来暂停执行,并且恢复执行所需的数据与堆栈分开存储。这允许异步执行的顺序代码(例如,在没有显式回调的情况下处理非阻塞I/O),还支持惰性计算无限序列上的算法和其他用途。
  如果函数的定义包含以下任何一项,则该函数就是协程:

  • co_await表达式 — 暂停执行直到恢复
task<> tcp_echo_server()
{
    char data[1024];
    while (true)
    {
        std::size_t n = co_await socket.async_read_some(buffer(data));
        co_await async_write(socket, buffer(data, n));
    }
}
  • co_yield 表达式 — 暂停执行返回值
generator<unsigned int> iota(unsigned int n = 0)
{
    while (true)
        co_yield n++;
}
  • co_return 语句 — 返回值以完成执行
lazy<int> f()
{
    co_return 7;
}

每个协程都必须有一个满足许多要求的返回类型,如下所述。

限制(Restrictions)

协程不能使用可变参数、纯返回语句或占位符返回类型(auto或Concept)。
Const eval函数、const expr函数、构造函数、析构函数和主函数不能是协程。

执行(Execution)

每个协程关联于:

  • promise对象,从协程内部进行操作。协程通过此对象提交其结果或异常。Promise对象与std::Promise没有任何关系。
  • 从协程程序外部操作的协程程序句柄。这是一个非拥有句柄,用于恢复协程的执行或销毁协程框架。
  • 协程状态,它是内部动态分配的存储(除非优化了分配),包含:
    • promise对象
    • 参数(全部按值复制)
    • 当前挂起点的一些表示,这样知道在哪里继续,销毁知道范围内的局部变量.
    • 局部变量和临时变量,它们的寿命跨越当前暂停点。

当协程开始执行时,它执行以下操作:

  • 使用运算符new分配协程状态对象。
  • 将所有函数参数复制到协程状态:按值参数被移动或复制,按引用参数保留引用(因此,如果在引用对象的生存期结束后恢复协程,则可能会变成悬空的——参见下面的示例)。
  • 调用promise对象的构造函数。如果promise类型有一个接受所有协程参数的构造函数,则调用该构造函数,并使用复制后的协程参数。否则将调用默认构造函数。
  • 调用promise.get_return_object()并将结果保存在局部变量中。当协程首次挂起时,该调用的结果将返回给调用者。在该步骤之前(包括该步骤)引发的任何异常都会传播回调用方,而不是放在promise中。
  • 调用promise.initial_suspend(),co_awaits其结果。典型的Promise类型要么返回std::suspend_allways,用于延迟启动的协程,要么返回std::suspend_never,用于急切启动的协程。
  • 当co-await promise.initial_suspend()恢复时,开始执行协程的主体。

参数悬空的一些示例:

#include <coroutine>
#include <iostream>
 
struct promise;
 
struct coroutine : std::coroutine_handle<promise>
{
    using promise_type = ::promise;
};
 
struct promise
{
    coroutine get_return_object() { return {coroutine::from_promise(*this)}; }
    std::suspend_always initial_suspend() noexcept { return {}; }
    std::suspend_always final_suspend() noexcept { return {}; }
    void return_void() {}
    void unhandled_exception() {}
};
 
struct S
{
    int i;
    coroutine f()
    {
        std::cout << i;
        co_return;
    }
};
 
void bad1()
{
    coroutine h = S{0}.f();
    // S{0} destroyed
    // S{0} 已销毁
    h.resume(); // resumed coroutine executes std::cout << i, uses S::i after free
                // 恢复的协程执行std::cout << i ,释放后使用S::i
    h.destroy();
}
 
coroutine bad2()
{
    S s{0};
    return s.f(); // returned coroutine can't be resumed without committing use after free
                  // 返回的协程在释放后未提交使用就无法恢复
}
 
void bad3()
{
    coroutine h = [i = 0]() -> coroutine // a lambda that's also a coroutine
                                         // 一个lambda也是一个协程
    {
        std::cout << i;
        co_return;
    }(); // immediately invoked
         // 立即调用
    // lambda destroyed
    // lambda已销毁
    h.resume(); // uses (anonymous lambda type)::i after free
                // 释放后使用(匿名lambda类型)::i
    h.destroy();
}
 
void good()
{
    coroutine h = [](int i) -> coroutine // make i a coroutine parameter
                                         // 使i成为协程参数
    {
        std::cout << i;
        co_return;
    }(0);
    // lambda destroyed
    // ambda已销毁
    h.resume(); // no problem, i has been copied to the coroutine
                // frame as a by-value parameter
                //没问题,i作为值参数已经被复制到协同程序了
    h.destroy();
}

当协程程序达到暂停点时:

  • 如果需要,在隐式转换为协程的返回类型之后,将先前获得的返回对象返回给caller/resumer。
    当协程到达co_return语句时,它将执行以下操作:
  • 为下了函数调用promise.return_void():
    • co_return
    • co_return expr; 其中expr的类型为void
  • 或为co_return expr调用promise.return_value(expr);其中expr具有非void类型
  • 以与创建顺序相反的顺序销毁所有具有自动存储持续时间的变量。
  • 调用promise.final_suspend(),co_awaits结果。

从协程的末尾脱落相当于co_return,除了如果在Promise的范围中找不到return_void的声明,则行为是未定义的。函数体中没有定义关键字的函数不是协程,无论其返回类型如何,如果返回类型不是(可能是cv限定的)void,则从末尾掉下来会导致未定义的行为。

// assuming that task is some coroutine task type
// 假设task是某种协程任务类型
task<void> f()
{
    // not a coroutine, undefined behavior
    // 不是协程,未定义的行为
}
 
task<void> g()
{
    co_return;  // OK
}
 
task<void> h()
{
    co_await g();
    // OK, implicit co_return;
    // OK, 隐式co_return
}

如果协程程序以未捕获的异常结束,它将执行以下操作:

  • 捕获异常并从catch块中调用promise.unhanded_exception()
  • 调用promise.final_suspend(),co_wait结果(例如,恢复延续或发布结果)。从这一点开始恢复协程程序是未定义的行为。
    当协程状态被破坏时,无论是因为它通过co_return或未捕获异常终止,还是因为它通过其句柄被破坏,它都会执行以下操作:
  • 调用promise对象的析构函数。
  • 调用函数参数副本的析构函数。
  • 调用操作符delete来释放协程状态所使用的内存。
  • 将执行转移回caller/resumer。

动态分配(Dynamic allocation)

通过非数组运算符new动态分配协程状态。
如果Promise类型定义了类级替换,则将使用它,否则将使用全局运算符new。
如果Promise类型定义了一个采用额外参数的运算符new的放置形式,并且它们与一个参数列表匹配,其中第一个参数是请求的大小(类型为std::size_t),其余的是协程函数参数,则这些参数将传递给运算符new(这使得可以对协程使用前导分配器约定)。
可以优化对运算符new的调用(即使使用了自定义分配器)如果

  • 协程状态的生存期严格嵌套在调用程序的生存期内,并且
  • 协程框架的大小在调用侧是已知的。

在这种情况下,协程状态嵌入到调用方的堆栈帧中(如果调用方是普通函数)或协程状态中(如果调用者是协程)。
如果分配失败,协程将抛出std::bad_alloc,除非Promise类型定义了成员函数Promise::get_return_object_on_allocation_failure()。如果定义了该成员函数,则分配使用另一种形式的运算符new,并且在分配失败时,协程会立即将从Promise::get_return_object_on_allocation_failure()获得的对象返回给调用方,例如:

struct Coroutine::promise_type
{
    /* ... */
 
    // ensure the use of non-throwing operator-new
    // 确保使用新的非异常操作符new
    static Coroutine get_return_object_on_allocation_failure()
    {
        std::cerr << __func__ << '\n';
        throw std::bad_alloc(); // or, return Coroutine(nullptr);
    }
 
    // custom non-throwing overload of new
    // 自定义非异常操作符new的重载
    void* operator new(std::size_t n) noexcept
    {
        if (void* mem = std::malloc(n))
            return mem;
        return nullptr; // allocation failure
                        // 分配失败
    }
};

Promise

Promise类型由编译器根据使用std::coroutine_tracts的协程的返回类型来确定。
正规的,让:

  • R和Args…分别表示协程的返回类型和参数类型列表,
  • ClassT表示协程所属的类类型,如果它被定义为非静态成员函数,
  • cv表示在函数声明中声明的cv资格,如果它被定义为非静态成员函数,
    其Promise类型由以下因素决定:
  • std::coroutine_traits<R, Args…>::promise_type, 如果协程没有被定义为隐式对象成员函数,
  • std::coroutine_traits<R, cv ClassT&, Args…>::promise_type,如果协程被定义为不是右值引用限定的隐式对象成员函数,
  • std::coroutine_traits<R, cv ClassT&&, Args…>::promise_type,如果协程被定义为一个隐式对象成员函数,该函数是右值引用限定的。
    例如:
If the coroutine is defined as …then its Promise type is …
task foo(int x);std::coroutine_traits<task, int>::promise_type
task Bar::foo(int x) const;std::coroutine_traits<task, const Bar&, int>::promise_type
task Bar::foo(int x) &&;std::coroutine_traits<task, Bar&&, int>::promise_type

co_await

一元运算符co_await挂起协程并将控制权返回给调用者。它的操作数是一个表达式,(1)是定义成员运算符co_await的类类型,或者可以传递给非成员运算符co_await,或者(2)可以通过当前协程的Promise::await_transform转换为这样的类类型。

co_await expr

co_await表达式只能出现在正则函数体(包括lambda表达式的函数体)中的潜在求值表达式中,而不能出现:

  • 在handler中
  • 在声明语句中,除非它出现在该声明语句的初始值设定项中,
  • 在init语句的简单声明中(参见if、switch、for和range for),除非它出现在该init语句的初始值设定项中,
  • 在默认参数中,或者
  • 在具有静态或线程存储持续时间的块作用域变量的初始值设定项中。

首先,expr转换为awaitable,如下所示:

  • 如果expr是由初始挂起点、最终挂起点或yield表达式生成的,那么awaitable就是expr。
  • 否则,如果当前协程的Promise类型具有成员函数await_transform,那么awaitable是Promise.await_transform(expr)。
  • 否则,awaitable就是expr。

然后,获得awaiter对象,如下所示:

  • 如果运算符co_await的重载解析给出单个最佳过载,则awaiter是该调用的结果:
    • awaitable.operator co_await()用于成员重载
    • operator co_await(static_cast<Awaitable&&>(awaitable))用于非成员重载
  • 否则,如果重载解析没有找到运算符co_await,则awaiter是awaitable的。
  • 否则,如果重载解析不明确,则表示程序格式不正确。

如果上面的表达式是右值,则awaiter对象是从临时对象构建的。否则,如果上面的公式是glvalue,则awaiter对象就是它所引用的对象。

然后,调用awaiter.await_ready(如果知道结果已经准备好或可以同步完成,这是避免挂起成本的捷径)。如果其上下文转换为布尔的结果为false,则协程被挂起(它的协程状态由局部变量和当前挂接点填充)。
调用awaiter.await_suspend(handle),其中handle是表示当前协程的协程句柄。在该函数中,通过该句柄可以观察到挂起的协程状态,该函数的职责是将其调度为在某个执行器上恢复,或者销毁(返回错误计数作为调度)

  • 如果await_suspend返回void,则控制权将立即返回给当前协程的caller/resumer(此协程保持挂起状态),否则
  • 如果await_suspend返回bool,
    • 值true将控制权返回给当前协同程序的caller/resumer
    • 值false将恢复当前的协同程序。
  • 如果await_suspend为其他协程返回一个协程句柄,则该句柄将被恢复(通过调用handle.resume())(注意,这可能会连锁,最终导致当前协程恢复)。
  • 如果await_suspend抛出异常,则会捕获该异常,恢复协程,并立即重新抛出该异常。

最后,调用awaiter.await_resume()(无论协程是否已挂起),其结果是整个co_await expr表达式的结果。

如果协程在co-await表达式中被挂起,随后又被恢复,则恢复点就在调用awaiter.await_resume()之前。

请注意,由于协程在输入awaiter.await_suspend()之前已完全挂起,因此该函数可以自由地跨线程传输协程句柄,而无需额外的同步。例如,它可以将其放入回调中,计划在异步I/O操作完成时在线程池上运行。在这种情况下,由于当前协程可能已经恢复并因此执行了awaiter对象的析构函数,所有这些都是在await_suspend()在当前线程上继续执行的同时进行的,await_suspend()应该将*this视为已销毁,并且在句柄发布到其他线程后不访问它。
实例:

#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>
 
auto switch_to_new_thread(std::jthread& out)
{
    struct awaitable
    {
        std::jthread* p_out;
        bool await_ready() { return false; }
        void await_suspend(std::coroutine_handle<> h)
        {
            std::jthread& out = *p_out;
            if (out.joinable())
                throw std::runtime_error("Output jthread parameter not empty");
            out = std::jthread([h] { h.resume(); });
            // Potential undefined behavior: accessing potentially destroyed *this
            // 潜在的未定义行为:访问可能被破坏的*this
            // std::cout << "New thread ID: " << p_out->get_id() << '\n';
            std::cout << "New thread ID: " << out.get_id() << '\n'; // this is OK
        }
        void await_resume() {}
    };
    return awaitable{&out};
}
 
struct task
{
    struct promise_type
    {
        task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
};
 
task resuming_on_new_thread(std::jthread& out)
{
    std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n';
    co_await switch_to_new_thread(out);
    // awaiter destroyed here
    std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n';
}
 
int main()
{
    std::jthread out;
    resuming_on_new_thread(out);
}

运行结果:

Coroutine started on thread: 139972277602112
New thread ID: 139972267284224
Coroutine resumed on thread: 139972267284224

注意:awaiter对象是协程状态的一部分(作为一个临时对象,其生存期跨越挂起点),并在co_await表达式完成之前被销毁。它可以用于根据某些异步I/O API的要求维护每个操作的状态,而不需要额外的动态分配。
标准库定义了两个简单的awaitable:std::suspend_allways和std::suspend_never。
此部分不完整原因:示例

Demo of promise_type::await_transform and a program provided awaiter

#include <cassert>
#include <coroutine>
#include <iostream>
 
struct tunable_coro
{
    // An awaiter whose "readiness" is determined via constructor's parameter.
    // 通过构造函数的参数确定其“就绪状态”的awaiter。
    class tunable_awaiter
    {
        bool ready_;
    public:
        explicit(false) tunable_awaiter(bool ready) : ready_{ready} {}
        // Three standard awaiter interface functions:
        // 三个标准awaiter接口函数:
        bool await_ready() const noexcept { return ready_; }
        static void await_suspend(std::coroutine_handle<>) noexcept {}
        static void await_resume() noexcept {}
    };
 
    struct promise_type
    {
        using coro_handle = std::coroutine_handle<promise_type>;
        auto get_return_object() { return coro_handle::from_promise(*this); }
        static auto initial_suspend() { return std::suspend_always(); }
        static auto final_suspend() noexcept { return std::suspend_always(); }
        static void return_void() {}
        static void unhandled_exception() { std::terminate(); }
        // A user provided transforming function which returns the custom awaiter:
        // 一个用户提供的转换函数,它返回自定义的awaiter:
        auto await_transform(std::suspend_always) { return tunable_awaiter(!ready_); }
        void disable_suspension() { ready_ = false; }
    private:
        bool ready_{true};
    };
 
    tunable_coro(promise_type::coro_handle h) : handle_(h) { assert(h); }
 
    // For simplicity, declare these 4 special functions as deleted:
    // 为了简单起见,将这4个特殊函数声明为已删除:
    tunable_coro(tunable_coro const&) = delete;
    tunable_coro(tunable_coro&&) = delete;
    tunable_coro& operator=(tunable_coro const&) = delete;
    tunable_coro& operator=(tunable_coro&&) = delete;
 
    ~tunable_coro()
    {
        if (handle_)
            handle_.destroy();
    }
 
    void disable_suspension() const
    {
        if (handle_.done())
            return;
        handle_.promise().disable_suspension();
        handle_();
    }
 
    bool operator()()
    {
        if (!handle_.done())
            handle_();
        return !handle_.done();
    }
private:
    promise_type::coro_handle handle_;
};
 
tunable_coro generate(int n)
{
    for (int i{}; i != n; ++i)
    {
        std::cout << i << ' ';
        // The awaiter passed to co_await goes to promise_type::await_transform which
        // issues tunable_awaiter that initially causes suspension (returning back to
        // main at each iteration), but after a call to disable_suspension no suspension
        // happens and the loop runs to its end without returning to main().
        //传递给co_await的awaiter将转到promise_type::await_transform
        //发出最初导致挂起的tunable_awaider(返回到
        //每次迭代时的main),但在调用disable_ssuspension后没有挂起
        //发生,循环运行到最后而不返回main()。
        co_await std::suspend_always{};
    }
}
 
int main()
{
    auto coro = generate(8);
    coro(); // emits only one first element == 0
    for (int k{}; k < 4; ++k)
    {
        coro(); // emits 1 2 3 4, one per each iteration
        std::cout << ": ";
    }
    coro.disable_suspension();
    coro(); // emits the tail numbers 5 6 7 all at ones
}

输出:

0 1 : 2 : 3 : 4 : 5 6 7

co_yield

co_yield表达式向调用方返回一个值并挂起当前的协程:它是可恢复生成器函数的公共构建块。
co_yield expr
co_yield braced-init-list
它相当于:

co_await promise.yield_value(expr)

一个典型的生成器的yield_value会将其参数存储(复制/移动或仅存储的地址,因为参数的生存期跨越了co_await内的挂起点)到生成器对象中,并返回std::suspend_allways,将控制权转移到调用方/返回方。

#include <coroutine>
#include <cstdint>
#include <exception>
#include <iostream>
 
template<typename T>
struct Generator
{
    // The class name 'Generator' is our choice and it is not required for coroutine
    // magic. Compiler recognizes coroutine by the presence of 'co_yield' keyword.
    // You can use name 'MyGenerator' (or any other name) instead as long as you include
    // nested struct promise_type with 'MyGenerator get_return_object()' method.
    //类名“Generator”是我们的选择,它不是协程所必需的魔术
    //编译器通过关键字“co_yield”的存在来识别协程程序。
    //您可以使用名称“MyGenerator”(或任何其他名称),只要您包含
    //带有"MyGenerator get_return_object()"方法的嵌套结构promise_type。
 
    struct promise_type;
    using handle_type = std::coroutine_handle<promise_type>;
 
    struct promise_type // required
    {
        T value_;
        std::exception_ptr exception_;
 
        Generator get_return_object()
        {
            return Generator(handle_type::from_promise(*this));
        }
        std::suspend_always initial_suspend() { return {}; }
        std::suspend_always final_suspend() noexcept { return {}; }
        void unhandled_exception() { exception_ = std::current_exception(); } // saving
                                                                              // exception
 
        template<std::convertible_to<T> From> // C++20 concept
        std::suspend_always yield_value(From&& from)
        {
            value_ = std::forward<From>(from); // caching the result in promise
            return {};
        }
        void return_void() {}
    };
 
    handle_type h_;
 
    Generator(handle_type h) : h_(h) {}
    ~Generator() { h_.destroy(); }
    explicit operator bool()
    {
        fill(); // The only way to reliably find out whether or not we finished coroutine,
                // whether or not there is going to be a next value generated (co_yield)
                // in coroutine via C++ getter (operator () below) is to execute/resume
                // coroutine until the next co_yield point (or let it fall off end).
                // Then we store/cache result in promise to allow getter (operator() below
                // to grab it without executing coroutine).
                //唯一可靠地查明我们是否完成了协程的方法,
                //是否会生成下一个值(co_yield)
                //在协程序中,通过C++ getter(下面的operator())执行/恢复
                //协程,直到下一个co_yield点(或者让它落下)。
                //然后,我们将结果存储/缓存在promise中,以允许下面的getter(operator())
                //在不执行协程的情况下获取它)。
        return !h_.done();
    }
    T operator()()
    {
        fill();
        full_ = false; // we are going to move out previously cached
                       // result to make promise empty again
        return std::move(h_.promise().value_);
    }
 
private:
    bool full_ = false;
 
    void fill()
    {
        if (!full_)
        {
            h_();
            if (h_.promise().exception_)
                std::rethrow_exception(h_.promise().exception_);
            // propagate coroutine exception in called context
            // 在调用的上下文中传播协同程序异常
 
            full_ = true;
        }
    }
};
 
Generator<std::uint64_t>
fibonacci_sequence(unsigned n)
{
    if (n == 0)
        co_return;
 
    if (n > 94)
        throw std::runtime_error("Too big Fibonacci sequence. Elements would overflow.");
 
    co_yield 0;
 
    if (n == 1)
        co_return;
 
    co_yield 1;
 
    if (n == 2)
        co_return;
 
    std::uint64_t a = 0;
    std::uint64_t b = 1;
 
    for (unsigned i = 2; i < n; ++i)
    {
        std::uint64_t s = a + b;
        co_yield s;
        a = b;
        b = s;
    }
}
 
int main()
{
    try
    {
        auto gen = fibonacci_sequence(10); // max 94 before uint64_t overflows
 
        for (int j = 0; gen; ++j)
            std::cout << "fib(" << j << ")=" << gen() << '\n';
    }
    catch (const std::exception& ex)
    {
        std::cerr << "Exception: " << ex.what() << '\n';
    }
    catch (...)
    {
        std::cerr << "Unknown exception.\n";
    }
}

输出结果:

fib(0)=0
fib(1)=1
fib(2)=1
fib(3)=2
fib(4)=3
fib(5)=5
fib(6)=8
fib(7)=13
fib(8)=21
fib(9)=34
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值