名稱空間
變體
操作

協程 (C++20)

來自 cppreference.com
< cpp‎ | 語言
 
 
C++ 語言
 
 

協程是一種可以暫停執行並在以後恢復的函式。協程是無棧的:它們透過返回給呼叫者來暫停執行,並且恢復執行所需的資料與棧分開儲存。這允許順序程式碼以非同步方式執行(例如,在沒有顯式回撥的情況下處理非阻塞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;
}

每個協程都必須有一個滿足以下要求的返回型別。

目錄

[編輯] 限制

協程不能使用可變引數、普通的return語句或佔位符返回型別autoConcept)。

Consteval 函式constexpr 函式建構函式解構函式main 函式不能是協程。

[編輯] 執行

每個協程都與以下物件關聯:

  • promise 物件,在協程內部操作。協程透過此物件提交其結果或異常。Promise 物件與std::promise沒有任何關聯。
  • 協程控制代碼,在協程外部操作。這是一個非擁有控制代碼,用於恢復協程的執行或銷燬協程幀。
  • 協程狀態,這是一個內部的、動態分配的儲存(除非分配被最佳化掉),包含:
  • promise 物件
  • 引數(全部按值複製)
  • 當前暫停點的一些表示,以便恢復時知道從何處繼續,銷燬時知道哪些區域性變數在作用域內
  • 生命週期跨越當前暫停點的區域性變數和臨時變數。

當協程開始執行時,它會執行以下操作:

  • 使用operator new分配協程狀態物件。
  • 將所有函式引數複製到協程狀態:按值引數被移動或複製,按引用引數仍然是引用(因此,如果在引用物件的生命週期結束後協程恢復,可能會變為懸空引用——請參閱下面的示例)。
  • 呼叫 promise 物件的建構函式。如果 promise 型別有一個接受所有協程引數的建構函式,則使用複製後的協程引數呼叫該建構函式。否則呼叫預設建構函式。
  • 呼叫promise.get_return_object()並將結果儲存在區域性變數中。當協程首次暫停時,該呼叫的結果將返回給呼叫者。在此步驟之前(含此步驟)丟擲的任何異常都會傳播回撥用者,而不是放入 promise 中。
  • 呼叫promise.initial_suspend()co_await其結果。典型的Promise型別要麼返回std::suspend_always(用於惰性啟動的協程),要麼返回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
    h.resume(); // resumed coroutine executes std::cout << i, uses S::i after free
    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
    {
        std::cout << i;
        co_return;
    }(); // immediately invoked
    // lambda destroyed
    h.resume(); // uses (anonymous lambda type)::i after free
    h.destroy();
}
 
void good()
{
    coroutine h = [](int i) -> coroutine // make i a coroutine parameter
    {
        std::cout << i;
        co_return;
    }(0);
    // lambda destroyed
    h.resume(); // no problem, i has been copied to the coroutine
                // frame as a by-value parameter
    h.destroy();
}

當協程到達暫停點時

  • 之前獲得的返回物件會返回給呼叫者/恢復者,如果需要,還會隱式轉換為協程的返回型別。

當協程到達co_return語句時,它執行以下操作:

  • 對於以下情況,呼叫promise.return_void()
  • co_return;
  • co_return expr;其中expr的型別是void
  • 或者對於co_return expr;,如果expr是非void型別,則呼叫promise.return_value(expr)
  • 以相反的建立順序銷燬所有具有自動儲存持續時間的變數。
  • 呼叫promise.final_suspend()co_await其結果。

協程執行到末尾等同於co_return;,除非在Promise的作用域中找不到return_void的宣告,這種情況下行為是未定義的。如果函式的函式體中沒有定義協程關鍵字,則無論其返回型別如何,它都不是協程,並且如果返回型別不是(可能帶有cv限定符的)void,則執行到末尾會導致未定義行為。

// assuming that task is some coroutine task type
task<void> f()
{
    // not a coroutine, undefined behavior
}
 
task<void> g()
{
    co_return;  // OK
}
 
task<void> h()
{
    co_await g();
    // OK, implicit co_return;
}

如果協程因未捕獲的異常而終止,它將執行以下操作:

  • 捕獲異常並在catch塊內呼叫promise.unhandled_exception()
  • 呼叫promise.final_suspend()co_await其結果(例如,恢復後續操作或釋出結果)。從這一點恢復協程是未定義行為。

當協程狀態因co_return或未捕獲的異常而終止,或者透過其控制代碼被銷燬時,它將執行以下操作:

  • 呼叫 promise 物件的解構函式。
  • 呼叫函式引數副本的解構函式。
  • 呼叫operator delete以釋放協程狀態使用的記憶體。
  • 將執行權返回給呼叫者/恢復者。

[編輯] 動態分配

協程狀態透過非陣列operator new動態分配。

如果Promise型別定義了類級別的替換,則將使用它,否則將使用全域性operator new

如果Promise型別定義了帶有額外引數的operator new的放置形式,並且它們與引數列表匹配(其中第一個引數是請求的大小,型別為std::size_t,其餘引數是協程函式引數),這些引數將傳遞給operator new(這使得可以為協程使用前置分配器約定)。

如果滿足以下條件,對operator new的呼叫可以被最佳化掉(即使使用了自定義分配器):

  • 協程狀態的生命週期嚴格巢狀在呼叫者的生命週期內,並且
  • 協程幀的大小在呼叫點已知。

在這種情況下,協程狀態嵌入在呼叫者的棧幀中(如果呼叫者是普通函式)或協程狀態中(如果呼叫者是協程)。

如果分配失敗,協程丟擲std::bad_alloc,除非Promise型別定義了成員函式Promise::get_return_object_on_allocation_failure()。如果定義了該成員函式,分配將使用operator new的nothrow形式,並且在分配失敗時,協程立即將從Promise::get_return_object_on_allocation_failure()獲得的物件返回給呼叫者,例如:

struct Coroutine::promise_type
{
    /* ... */
 
    // ensure the use of non-throwing operator-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
    void* operator new(std::size_t n) noexcept
    {
        if (void* mem = std::malloc(n))
            return mem;
        return nullptr; // allocation failure
    }
};

[編輯] Promise

Promise型別由編譯器根據協程的返回型別使用std::coroutine_traits確定。

正式地,令

  • RArgs... 分別表示協程的返回型別和引數型別列表,
  • ClassT 表示如果協程被定義為非靜態成員函式,則其所屬的類型別,
  • cv 表示如果協程被定義為非靜態成員函式,則在函式宣告中宣告的cv限定符,

Promise型別由以下方式確定:

例如:

如果協程定義為... 那麼它的Promise型別是...
task<void> foo(int x); std::coroutine_traits<task<void>, int>::promise_type
task<void> Bar::foo(int x) const; std::coroutine_traits<task<void>, const Bar&, int>::promise_type 
task<void> Bar::foo(int x) &&; std::coroutine_traits<task<void>, Bar&&, int>::promise_type

[編輯] co_await

一元運算子co_await暫停協程並將控制權返回給呼叫者。

co_await expr

co_await表示式只能出現在常規函式體(包括lambda表示式的函式體)中的潛在求值表示式中,並且不能出現在:

co_await表示式不能是潛在求值的子表示式,作為契約斷言的謂詞。

(C++26 起)

首先,expr 轉換為一個可等待物件,轉換方式如下:

  • 如果expr是由初始暫停點、最終暫停點或yield表示式生成的,則可等待物件是expr本身。
  • 否則,如果當前協程的Promise型別有成員函式await_transform,則可等待物件是promise.await_transform(expr)
  • 否則,可等待物件是expr本身。

然後,獲取等待器物件,方式如下:

  • 如果operator co_await的過載決議得到一個最佳過載,則等待器是該呼叫的結果
  • awaitable.operator co_await() 對於成員過載,
  • operator co_await(static_cast<Awaitable&&>(awaitable)) 對於非成員過載。
  • 否則,如果過載決議找不到operator co_await,則等待器是awaitable本身。
  • 否則,如果過載決議存在歧義,程式格式錯誤。

如果上述表示式是一個prvalue,則等待器物件是一個從其實體化的臨時物件。否則,如果上述表示式是一個glvalue,則等待器物件是它引用的物件。

然後,呼叫awaiter.await_ready()(這是一個捷徑,如果已知結果已就緒或可以同步完成,則避免暫停的開銷)。如果其結果在上下文轉換為bool後為false,則:

協程被暫停(其協程狀態填充了局部變數和當前暫停點)。
呼叫awaiter.await_suspend(handle),其中handle是表示當前協程的協程控制代碼。在該函式內部,可以透過該控制代碼觀察暫停的協程狀態,並且該函式負責安排它在某個執行器上恢復執行,或者銷燬它(返回false算作排程)。
  • 如果await_suspend返回void,控制權立即返回給當前協程的呼叫者/恢復者(此協程保持暫停),否則
  • 如果await_suspend返回bool
  • true將控制權返回給當前協程的呼叫者/恢復者
  • false恢復當前協程。
  • 如果await_suspend返回其他協程的協程控制代碼,則恢復該控制代碼(透過呼叫handle.resume())(請注意,這最終可能導致當前協程恢復)。
  • 如果await_suspend丟擲異常,則捕獲異常,恢復協程,並立即重新丟擲異常。

最後,呼叫awaiter.await_resume()(無論協程是否暫停),其結果是整個co_await expr表示式的結果。

如果協程在co_await表示式中被暫停,並且稍後恢復,則恢復點緊鄰在呼叫awaiter.await_resume()之前。

請注意,協程在進入awaiter.await_suspend()之前已完全暫停。它的控制代碼可以與另一個執行緒共享,並在await_suspend()函式返回之前恢復。(請注意,預設的記憶體安全規則仍然適用,因此,如果協程控制代碼在沒有鎖的情況下跨執行緒共享,則等待器應至少使用釋放語義,而恢復器應至少使用獲取語義。)例如,協程控制代碼可以放入回撥中,當非同步I/O操作完成時,安排線上程池上執行。在這種情況下,由於當前協程可能已恢復並執行了等待器物件的解構函式,所有這些都與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
            // 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

注意:等待器物件是協程狀態的一部分(作為生命週期跨越暫停點的臨時變數),並在co_await表示式結束前被銷燬。它可以用於維護某些非同步I/O API所需的操作狀態,而無需額外的動態分配。

標準庫定義了兩個簡單的可等待物件:std::suspend_alwaysstd::suspend_never

promise_type::await_transform和程式提供的等待器的演示

[編輯] 示例

#include <cassert>
#include <coroutine>
#include <iostream>
 
struct tunable_coro
{
    // An awaiter whose "readiness" is determined via constructor's parameter.
    class tunable_awaiter
    {
        bool ready_;
    public:
        explicit(false) tunable_awaiter(bool ready) : ready_{ready} {}
        // Three standard awaiter interface functions:
        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:
        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:
    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 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_always,將控制權轉移給呼叫者/恢復者。

#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.
    // (Note: It is necessary to adjust the declarations of constructors and destructors
    //  when renaming.)
 
    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).
        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

[編輯] 注意

特性測試 標準 特性
__cpp_impl_coroutine 201902L (C++20) 協程 (編譯器支援)
__cpp_lib_coroutine 201902L (C++20) 協程 (庫支援)
__cpp_lib_generator 202207L (C++23) std::generator:用於範圍的同步協程生成器

[編輯] 關鍵字

co_await, co_return, co_yield

[編輯] 庫支援

協程支援庫定義了幾種型別,為協程提供編譯時和執行時支援。

[編輯] 缺陷報告

下列更改行為的缺陷報告追溯地應用於以前出版的 C++ 標準。

缺陷報告 應用於 釋出時的行為 正確的行為
CWG 2556 C++20 無效的return_void導致
協程執行到末尾的行為未定義
程式格式錯誤
在這種情況下,列舉是病態的
CWG 2668 C++20 co_await不能出現在lambda表示式中 允許
CWG 2754 C++23 為顯式物件成員函式構造promise物件時,
獲取*this
在這種情況下,*this不被
獲取

[編輯] 另請參閱

(C++23)
表示同步協程生成器的view
(類模板) [編輯]

[編輯] 外部連結

1.  Lewis Baker, 2017-2022 - 非對稱傳輸 (Asymmetric Transfer)。
2.  David Mazières, 2021 - C++20 協程教程 (Tutorial on C++20 coroutines)。
3.  許川起 & 齊宇 & 韓瑤, 2021 - C++20 協程原理與應用 (C++20 Principles and Applications of Coroutine)。
4.  Simon Tatham, 2023 - 編寫自定義 C++20 協程系統 (Writing custom C++20 coroutine systems)。