[譯]C++ 協程:理解 co_await 運算符

C++ 協程:理解 co_await 運算符

在以前關於 協程理論的博客 中,我介紹了一些函數和協程在較高層次上的一些不一樣,但沒有詳細介紹 C++ 協程技術規範(N4680)中描述的語法和語義。前端

協程技術規範中,C++ 新增的關鍵新功能是可以掛起協程,並可以在以後恢復。技術規範爲此提供的機制是經過新的 co_await 運算符去實現。android

理解 co_await 運算符的工做原理能夠幫助咱們揭開協程行爲的神祕面紗,並瞭解它們如何被暫停和掛起的。在這篇文章中,我將解釋 co_await 操做符的機制,並介紹 AwaitableAwaiter 類型的相關概念。ios

在深刻講解 co_await 以前,我想簡要介紹一下協程的技術規範,以提供一些背景知識。c++

協程技術規範給咱們提供了什麼?

  • 三個新的關鍵字: co_awaitco_yieldco_return
  • std::experimental 命名空間的幾個新類型:
    • coroutine_handle<P>
    • coroutine_traits<Ts...>
    • suspend_always
    • suspend_never
  • 一種可以讓庫的做者與協程交互並定製它們行爲的通用機制。
  • 一個使異步代碼變得更加簡單的語言工具!

C++ 協程技術規範在語言中提供的工具,能夠理解爲協程的低級彙編語言。 這些工具很難直接以安全的方式使用,主要是供庫做者使用,用於構建應用程序開發人員能夠安全使用的更高級別的抽象。git

將來會將這些新的低級工具交付給即將到來的語言標準(多是 C++20),以及標準庫中伴隨的一些高級類型,這些高級類型封裝了這些低級構建塊,應用程序開發人員將能夠經過一種安全的方式輕鬆訪問協程。github

編譯器與庫的交互

有趣的是,協程技術規範實際上並無定義協程的語義。它沒有定義如何生成返回給調用者的值,沒有定義如何處理傳遞給 co_return 語句的返回值,如何處理傳遞出協程的異常,它也沒有定義應該恢復協程的線程。web

相反,它指定了庫代碼的通用機制,那就是經過實現符合特定接口的類型來定製協程的行爲。而後,編譯器生成代碼,在庫提供的類型實例上調用方法。這種方法相似於庫做者經過定義 begin() / end() 方法或 iterator 類型來定製基於範圍的 for 循環的實現。後端

協程技術規範沒有對協程的機制規定任何特定的語義,這使它成爲一個強大的工具。它容許庫做者爲各類不一樣目的來定義許多不一樣種類的協程。promise

例如,你能夠定義一個異步生成單個值的協程,或者一個延遲生成一系列值的協程,或者若是遇到 nullopt 值,則經過提早退出來簡化控制流以消耗 optional <T> 值的協程。安全

協程技術規範定義了兩種接口:Promise 接口和 Awaitable 接口。

Promise 接口指定用於自定義協程自己行爲的方法。庫做者可以自定義調用協程時發生的事件,如協程返回時(經過正常方式或經過未處理的異常返回),或者自定義協程中任何 co_awaitco_yield 表達式的行爲。

Awaitable 接口指定控制 co_await 表達式語義的方法。當一個值爲 co_await 時,代碼被轉換爲對 awaitable 對象上的方法的一系列調用。它能夠指定:是否暫停當前協程,暫停調度協程以便稍後恢復後執行一些邏輯,還有在協程恢復後執行一些邏輯以產生 co_await 表達式的結果。

我將在之後的博客中介紹 Promise 接口的細節,如今咱們先來看看 Awaitable 藉口。

Awaiters 與 Awaitables:解釋操做符 co_await

co_await運算符是一個新的一元運算符,能夠應用於一個值。例如:co_await someValue

co_await 運算符只能在協程的上下文中使用。這有點語義重複,由於根據定義,任何包含 co_await 運算符的函數體都將被編譯爲協程。

支持 co_await 運算符的類型稱爲 Awaitable 類型。

注意,co_await 運算符是否能夠用做類型取決於 co_await 表達式出現的上下文。用於協程的 promise 類型能夠經過其 await_transform 方法更改協程中的 co_await 表達式的含義(稍後將詳細介紹)。

爲了更具體地在須要的地方,我喜歡使用術語 Normally Awaitable 來描述在協程類型中沒有 await_transform 成員的協程上下文中支持 co_await 運算符的類型。我喜歡使用術語 Contextually Awaitable 來描述一種類型,它在某些類型的協程的上下文中僅支持 co_await 運算符,由於協程的 promise 類型中存在 await_transform 方法。(我樂意接受這些名字的更好建議...)

Awaiter 類型是一種實現三個特殊方法的類型,它們被稱爲 co_await 表達式的一部分:await_readyawait_suspendawait_resume

請注意,我在 C# async 關鍵字的機制中「借用」了 「Awaiter」 這個術語,該機制是根據 GetAwaiter() 方法實現的,該方法返回一個對象,其接口與 c++ 的 Awaiter 概念驚人的類似。有關 C# awaiters 的更多詳細信息,請參閱這篇博文

請注意,類型能夠是 Awaitable 類型和 Awaiter 類型。

當編譯器遇到 co_await <expr> 表達式時,實際上能夠根據所涉及的類型將其轉換爲許多可能的內容。

獲取 Awaiter

編譯器作的第一件事是生成代碼,以獲取等待值的 Awaiter 對象。在 N4680 章節 5.3.8(3) 中,有不少步驟能夠得到 awaiter。

讓咱們假設等待協程的 promise 對象具備類型 P,而且 promise 是對當前協程的 promise 對象的 l-value 引用。

若是 promise 類型 P 有一個名爲 await_transform 的成員,那麼 <expr> 首先被傳遞給 promise.await_transform(<expr>) 以得到 Awaitable 的值。 不然,若是 promise 類型沒有 await_transform 成員,那麼咱們使用直接評估 <expr> 的結果做爲 Awaitable 對象。

而後,若是 Awaitable 對象,有一個可用的運算符 co_await() 重載,那麼調用它來獲取 Awaiter 對象。 不然,awaitable 的對象被用做 awaiter 對象。

若是咱們將這些規則編碼到 get_awaitable()get_awaiter() 函數中,它們可能看起來像這樣:

template<typename P, typename T>
decltype(auto) get_awaitable(P& promise, T&& expr)
{
  if constexpr (has_any_await_transform_member_v<P>)
    return promise.await_transform(static_cast<T&&>(expr));
  else
    return static_cast<T&&>(expr);
}

template<typename Awaitable>
decltype(auto) get_awaiter(Awaitable&& awaitable)
{
  if constexpr (has_member_operator_co_await_v<Awaitable>)
    return static_cast<Awaitable&&>(awaitable).operator co_await();
  else if constexpr (has_non_member_operator_co_await_v<Awaitable&&>)
    return operator co_await(static_cast<Awaitable&&>(awaitable));
  else
    return static_cast<Awaitable&&>(awaitable);
}
複製代碼

等待 Awaiter

所以,假設咱們已經封裝了將 <expr> 結果轉換爲 Awaiter 對象到上述函數中的邏輯,那麼 co_await <expr> 的語義能夠(大體)這樣轉換:

{
  auto&& value = <expr>;
  auto&& awaitable = get_awaitable(promise, static_cast<decltype(value)>(value));
  auto&& awaiter = get_awaiter(static_cast<decltype(awaitable)>(awaitable));
  if (!awaiter.await_ready())
  {
    using handle_t = std::experimental::coroutine_handle<P>;

    using await_suspend_result_t =
      decltype(awaiter.await_suspend(handle_t::from_promise(p)));

    <suspend-coroutine>

    if constexpr (std::is_void_v<await_suspend_result_t>) {
      awaiter.await_suspend(handle_t::from_promise(p));
      <return-to-caller-or-resumer>
    }
    else
    {
      static_assert(
         std::is_same_v<await_suspend_result_t, bool>,
         "await_suspend() must return 'void' or 'bool'.");

      if (awaiter.await_suspend(handle_t::from_promise(p)))
      {
        <return-to-caller-or-resumer>
      }
    }

    <resume-point>
  }

  return awaiter.await_resume();
}
複製代碼

await_suspend() 的調用返回時,await_suspend() 的返回值爲 void 的版本無條件地將執行轉移回協程的調用者/恢復者,而返回值爲 bool 的版本容許 awaiter 對象有條件地返回並當即恢復協程,而不返回調用者/恢復者。

await_suspen()bool 返回版本在 awaiter 可能啓動異步操做(有時能夠同步完成)的狀況下很是有用。 在它同步完成的狀況下,await_suspend() 方法能夠返回 false 以指示應該當即恢復協程並繼續執行。

<suspend-coroutine> 處,編譯器生成一些代碼來保存協程的當前狀態並準備恢復。這包括存儲 <resume-point> 的斷點位置,以及將當前保存在寄存器中的任何值溢出到協程快照內存中。

<suspend-coroutine> 操做完成後,當前的協程被認爲是暫停的。你能夠觀察到暫停的協程的第一個斷點是在 await_suspend() 的調用中。協程暫停後,就能夠恢復或銷燬。

當操做完成後,await_suspend() 方法負責在未來的某個時刻調度並將協程恢復(或銷燬)。注意,從 await_suspend() 中返回 false 算做調度協程,以便在當前線程上當即恢復。

await_ready() 方法的目的,是容許你在已知操做同步完成而不須要掛起的狀況下避免 <suspend-coroutine> 操做的成本。

<return-to-caller-or-resumer> 斷點處執行轉移回調用者或恢復者,彈出本地堆棧幀但保持協程幀活躍。

當(或者說若是)暫停的協程最終恢復時,執行將在 <resume-point> 斷點處從新開始。即緊接在調用 await_resume()方法以前獲取操做的結果。

await_resume() 方法調用的返回值成爲 co_await 表達式的結果。await_resume() 方法也能夠拋出異常,在這種狀況下異常從 co_await 表達式中拋出。

注意,若是異常從 await_suspen() 拋出,則協程會自動恢復,而且異常會從 co_await 表達式拋出而不調用 await_resume()

協程句柄

你可能已經注意到 coroutine_handle <P> 類型的使用,該類型被傳遞給 co_await 表達式的 await_suspend() 調用。

該類型表示協程幀的非擁有句柄,可用於恢復協程的執行或銷燬協程幀。它還能夠用於訪問協程的 promise 對象。

coroutine_handle 類型具備如下接口:

namespace std::experimental
{
  template<typename Promise>
  struct coroutine_handle;

  template<>
  struct coroutine_handle<void> {
    bool done() const;

    void resume();
    void destroy();

    void* address() const;
    static coroutine_handle from_address(void* address);
  };

  template<typename Promise>
  struct coroutine_handle : coroutine_handle<void>
  {
    Promise& promise() const;
    static coroutine_handle from_promise(Promise& promise);

    static coroutine_handle from_address(void* address);
  };
}
複製代碼

在實現 Awaitable 類型時,你將在 coroutine_handle 上使用的主要方法是 .resume(),當操做完成並但願恢復等待的協程的執行時,應該調用這個方法。在 coroutine_handle 上調用 .resume() 將在 <resume-point> 從新喚醒一個掛起的協程。當協程接下來遇到一個 <return-to-caller-or-resumer> 時,對 .resume() 的調用將返回。

.destroy() 方法銷燬協程幀,調用任何範圍內變量的析構函數並釋放協程幀使用的內存。一般,你不須要(實際上應該是避免)調用 .destroy(),除非你是一個實現協程 promise 類型的庫編寫者。一般,協程幀將由從對協程的調用返回的某種 RAII(譯者注:資源獲取即初始化)類型擁有。 因此在沒有與 RAII 對象合做的狀況下調用 .destroy() 可能會致使雙重銷燬的錯誤。

.promise() 方法返回對協程的 promise 對象的引用。可是,就像 .destroy() 那樣,它一般只在你建立協程 promise 類型時纔有用。 你應該將協程的 promise 對象視爲協程的內部實現細節。 對於大多數常規的 Awaitable 類型,你應該使用 coroutine_handle <void> 做爲 await_suspend() 方法的參數類型,而不是 coroutine_handle <Promise>

coroutine_handle <P> :: from_promise(P&promise) 函數容許從對協程的 promise 對象的引用重構協程句柄。注意,你必須確保類型 P 與用於協程幀的具體 promise 類型徹底匹配; 當具體的 promise 類型是 Derived 時,試圖構造 coroutine_handle <Base> 會出現未定義的行爲的錯誤。

.address()/from_address() 函數容許將協程句柄轉換爲 void* 指針。這主要是爲了容許做爲 「context(上下文)」參數傳遞到現有的 C 風格的 API 中,所以你可能會發如今某些狀況下實現 Awaitable 類型頗有用。可是,在大多數狀況下,我發現有必要將附加信息傳遞給這個 'context' 參數中的回調,所以我一般最終將 coroutine_handle 存儲在結構中並將指針傳遞給 'context' 參數中的結構而不是使用 .address() 返回值。

無同步的異步代碼

co_await 運算符的一個強大的設計功能是在協程掛起以後但在執行返回給調用者/恢復者以前執行代碼的能力。

這容許 Awaiter 對象在協程已經被掛起以後發起異步操做,將被掛起的協程的(句柄) coroutine_handle 傳遞給運算符,當操做完成時(可能在另外一個線程上)它能夠安全地恢復操做,而不須要任何額外的同步。

例如,當協程已經掛起時,在 await_suspend() 內啓動異步讀操做意味着咱們能夠在操做完成時恢復協程,而不須要任何線程同步來協調啓動操做的線程和完成操做的線程。

Time     Thread 1                           Thread 2
  |      --------                           --------
  |      ....                               Call OS - Wait for I/O event
  |      Call await_ready()                    |
  |      <supend-point>                        |
  |      Call await_suspend(handle)            |
  |        Store handle in operation           |
  V        Start AsyncFileRead ---+            V
                                  +----->   <AsyncFileRead Completion Event>
                                            Load coroutine_handle from operation
                                            Call handle.resume()
                                              <resume-point>
                                              Call to await_resume()
                                              execution continues....
           Call to AsyncFileRead returns
         Call to await_suspend() returns
         <return-to-caller/resumer>
複製代碼

在利用這種方法時要特別注意的一件事情是,若是你開始將協程句柄發佈到其餘線程的操做,那麼另外一個線程能夠在 await_suspend() 返回以前恢復另外一個線程上的協程,繼續與 await_suspend() 方法的其他部分同時執行。

協程恢復時首先要作的是調用 await_resume() 來獲取結果,而後常常會當即銷燬 Awaiter 對象(即 await_suspend() 調用的 this 指針)。在 await_suspend() 返回以前,協程可能會運行完成,銷燬協程和 promise 對象。

因此在 await_suspend() 方法中,若是能夠在另外一個線程上同時恢復協程,你須要確保避免訪問 this 指針或協程的 .promise() 對象,由於二者都已經可能已被銷燬。通常來講,在啓動操做並計劃恢復協程以後,惟一能夠安全訪問的是 await_suspend() 中的局部變量。

與 Stackful 協程的比較

我想稍微多作一些說明,比較一下協程技術規範中的 stackless 協程在協程掛起後與一些現有的常見的協程工具(如 Win32 纖程或 boost::context )一塊兒執行邏輯的能力。

對於許多 stackful 協程框架,一個協程的暫停操做與另外一個協程的恢復操做相結合,造成一個 「context-switch(上下文切換)」 操做。使用這種 「context-switch」 操做,一般在掛起當前協程以後,而在將執行轉移到另外一個協程以前,沒有機會執行邏輯。

這意味着,若是咱們想在 stackful 協程之上實現相似的異步文件讀取操做,那麼咱們必須在掛起協程以前啓動操做。所以,能夠在協程暫停以前在另外一個線程上完成操做,而且有資格恢復。在另外一個線程上完成的操做和協程掛起之間的這種潛在競爭須要某種線程同步來仲裁,並決定勝利者。

經過使用 trampoline context 能夠解決這個問題,該上下文能夠在初始化上下文被掛起後表明啓動上下文啓動操做。然而,這將須要額外的基礎設施和額外的上下文切換以使其工做,而且這引入的開銷可能大於它試圖避免同步的成本。

避免內存分配

異步操做一般須要存儲一些每一個操做的狀態,以跟蹤操做的進度。這種狀態一般須要在操做期間持續,而且只有在操做完成後纔會釋放。

例如,調用異步 Win32 I/O 函數須要你分配並傳遞指向 OVERLAPPED 結構的指針。調用者負責確保此指針保持有效,直到操做完成。

使用傳統的基於回調的 API,一般須要在堆上分配此狀態以確保其具備適當的生命週期。若是你執行了許多操做,則可能須要爲每一個操做分配並釋放此狀態。若是性能成爲了問題,那麼可使用自定義分配器從內存池中分配這些狀態對象。

同時,咱們能夠在使用協程時,經過利用協程幀中的局部變量在協程掛起後還會保持活躍的特性,避免爲操做狀態在堆上分配內存。

經過將每一個操做狀態放置在 Awaiter 對象中,咱們能夠從協程幀有效地 「borrow(借用)」 存儲器,用於在 co_await 表達式的持續時間內存儲每一個操做狀態。一旦操做完成,協程就會恢復而且銷燬 Awaiter 對象,從而釋放協程幀中的內存以供其餘局部變量使用。

最終,協程幀仍然能夠在堆上分配。可是,一旦分配了,協程幀就可使用這個堆分配來執行許多異步操做。

你想一想,協程幀就像一種高性能的 arena 內存分配器。編譯器在編譯時計算出全部局部變量所需的 arena 總大小,而後可以根據須要將內存分配給局部變量,而開銷爲零!試着用自定義分配器戰勝它;)

示例:實現簡單的線程同步原語

既然咱們已經介紹了 co_await 運算符的許多機制,我想經過實現一個基本可等待同步原語來展現如何將這些知識付諸實踐:異步手動重置事件。

這個事件的基本要求是,它須要經過多個併發執行協程來成爲 Awaitable 狀態,當等待時,須要掛起等待的協程,直到某個線程調用 .set() 方法,此時任何等待的協程都將恢復。若是某個線程已經調用了 .set(),那麼協程應該繼續,而不是掛起。

理想狀況下,咱們還但願將其設置爲 noexcept,不須要在堆上分配,也不須要無鎖的實現。

2017/11/23 更新:增長 async_manual_reset_event 示例

示例用法以下所示:

T value;
async_manual_reset_event event;

// A single call to produce a value
void producer() {
  value = some_long_running_computation();

  // Publish the value by setting the event.
  event.set();
}

// Supports multiple concurrent consumers
task<> consumer()
{
  // Wait until the event is signalled by call to event.set()
  // in the producer() function.
  co_await event;

  // Now it's safe to consume 'value'
  // This is guaranteed to 'happen after' assignment to 'value'
  std::cout << value << std::endl;
}
複製代碼

讓咱們首先考慮一下這個事件可能存在的狀態:not setset

當它處於 'not set' 狀態時,有一隊(可能爲空的)協程正在等待它變爲 'set' 狀態。

當它處於 ‘set’ 狀態時,不會有任何等待的協程,由於 co_wait 狀態下的事件能夠在不暫停的狀況下繼續。

這個狀態實際上能夠用一個 std :: atomic <void *> 來表示。

  • 爲 ‘set’ 狀態保留一個特殊的指針值。在這種狀況下,咱們將使用事件的 this 指針,由於咱們知道不能與任何列表項相同的地址。
  • 不然,事件處於 ‘not set’ 狀態,而且該值是指向等待協程結構的單鏈表的頭部的指針。

咱們能夠經過將節點存儲在放置在協程幀內的 ‘awaiter’ 對象中,從而避免爲堆上的鏈表分配節點的額外調用。

讓咱們從一個類接口開始,以下所示:

class async_manual_reset_event {
public:

  async_manual_reset_event(bool initiallySet = false) noexcept;

  // No copying/moving
  async_manual_reset_event(const async_manual_reset_event&) = delete;
  async_manual_reset_event(async_manual_reset_event&&) = delete;
  async_manual_reset_event& operator=(const async_manual_reset_event&) = delete;
  async_manual_reset_event& operator=(async_manual_reset_event&&) = delete;

  bool is_set() const noexcept;

  struct awaiter;
  awaiter operator co_await() const noexcept;

  void set() noexcept;
  void reset() noexcept;

private:

  friend struct awaiter;

  // - 'this' => set state
  // - otherwise => not set, head of linked list of awaiter*.
  mutable std::atomic<void*> m_state;

};
複製代碼

咱們有一個至關直接和簡單的接口。在這一點上,須要關注的是它有一個 operator co_await() 方法,它返回了一個還沒有定義的 awaiter 類型。

如今讓咱們來定義 awaiter 類型

定義 Awaiter 類型

首先,它須要知道它將等待哪一個 async_manual_reset_event 的對象,所以它須要一個對這一事件和對應構造函數的應用來進行初始化。

它還須要充當 awaiter 值鏈表中的節點,所以它須要持有指向列表中下一個 awaiter 對象的指針。

它還須要存儲正在執行 co_await 表達式的等待協程的coroutine_handle,以便在事件變爲 'set' 狀態時事件能夠恢復協程。咱們不關心協程的 promise 類型是什麼,因此咱們只使用 coroutine_handle <>(這是 coroutine_handle <void> 的簡寫)。

最後,它須要實現 Awaiter 接口,所以須要三種特殊方法:await_readyawait_suspendawait_resume。 咱們不須要從 co_await 表達式返回一個值,所以 await_resume 能夠返回 void

當咱們將這些都放在一塊兒,awaiter 的基本類接口以下所示:

struct async_manual_reset_event::awaiter
{
  awaiter(const async_manual_reset_event& event) noexcept
  : m_event(event)
  {}

  bool await_ready() const noexcept;
  bool await_suspend(std::experimental::coroutine_handle<> awaitingCoroutine) noexcept;
  void await_resume() noexcept {}

private:

  const async_manual_reset_event& m_event;
  std::experimental::coroutine_handle<> m_awaitingCoroutine;
  awaiter* m_next;
};
複製代碼

如今,當咱們執行 co_await 一個事件時,若是事件已經設置,咱們不但願等待協程暫停。 所以,若是事件已經設置,咱們能夠定義 await_ready() 來返回 true

bool async_manual_reset_event::awaiter::await_ready() const noexcept
{
  return m_event.is_set();
}
複製代碼

接下來,讓咱們看一下 await_suspend() 方法。這一般是 awaitable 類型會發生莫名其妙的事情的地方。

首先,它須要將等待協程的句柄存入 m_awaitingCoroutine 成員,以便事件稍後能夠在其上調用 .resume()

而後,當咱們完成了這一步,咱們須要嘗試將 awaiter 自動加入到 waiters 的鏈表中。若是咱們成功加入它,而後咱們返回 true ,以代表咱們不想當即恢復協程,不然,若是咱們發現事件已併發地更改成 set 狀態,那麼咱們返回 false ,以代表協程應當即恢復。

bool async_manual_reset_event::awaiter::await_suspend(
  std::experimental::coroutine_handle<> awaitingCoroutine) noexcept
{
  // Special m_state value that indicates the event is in the 'set' state.
  const void* const setState = &m_event;

  // Remember the handle of the awaiting coroutine.
  m_awaitingCoroutine = awaitingCoroutine;

  // Try to atomically push this awaiter onto the front of the list.
  void* oldValue = m_event.m_state.load(std::memory_order_acquire);
  do
  {
    // Resume immediately if already in 'set' state.
    if (oldValue == setState) return false; 

    // Update linked list to point at current head.
    m_next = static_cast<awaiter*>(oldValue);

    // Finally, try to swap the old list head, inserting this awaiter
    // as the new list head.
  } while (!m_event.m_state.compare_exchange_weak(
             oldValue,
             this,
             std::memory_order_release,
             std::memory_order_acquire));

  // Successfully enqueued. Remain suspended.
  return true;
}
複製代碼

注意,在加載舊狀態時,咱們使用 'acquire' 查看內存順序,若是咱們讀取特殊的 'set' 值時,那麼咱們就能夠看到在調用 'set()' 以前發生的寫操做。

若是 compare-exchange 執行成功,咱們須要 ‘release’ 的狀態,以便後續的 ‘set()’ 調用將看到咱們對 m_awaitingconoutine 的寫入,以及以前對協程狀態的寫入。

補全事件類的其他部分

如今咱們已經定義了 awaiter 類型,讓咱們回過頭來看看 async_manual_reset_event 方法的實現。

首先是構造函數。它須要初始化爲 'not set' 狀態和空的 waiters 鏈表(即 nullptr)或初始化爲 'set' 狀態(即 this)。

async_manual_reset_event::async_manual_reset_event(
  bool initiallySet) noexcept
: m_state(initiallySet ? this : nullptr)
{}
複製代碼

接下來,is_set() 方法很是簡單 - 若是它具備特殊值 this,則爲 'set':

bool async_manual_reset_event::is_set() const noexcept
{
  return m_state.load(std::memory_order_acquire) == this;
}
複製代碼

而後是 reset() 方法,若是它處於 'set' 狀態,咱們但願它轉換爲 'not set' 狀態,不然保持原樣。

void async_manual_reset_event::reset() noexcept
{
  void* oldValue = this;
  m_state.compare_exchange_strong(oldValue, nullptr, std::memory_order_acquire);
}
複製代碼

使用 set() 方法,咱們但願經過使用特殊的 'set' 值(this)將當前狀態來轉換到 'set' 狀態,而後檢查本來的值是什麼。 若是有任何等待的協程,那麼咱們但願在返回以前依次順序恢復它們。

void async_manual_reset_event::set() noexcept
{
  // Needs to be 'release' so that subsequent 'co_await' has
  // visibility of our prior writes.
  // Needs to be 'acquire' so that we have visibility of prior
  // writes by awaiting coroutines.
  void* oldValue = m_state.exchange(this, std::memory_order_acq_rel);
  if (oldValue != this)
  {
    // Wasn't already in 'set' state.
    // Treat old value as head of a linked-list of waiters
    // which we have now acquired and need to resume.
    auto* waiters = static_cast<awaiter*>(oldValue);
    while (waiters != nullptr)
    {
      // Read m_next before resuming the coroutine as resuming
      // the coroutine will likely destroy the awaiter object.
      auto* next = waiters->m_next;
      waiters->m_awaitingCoroutine.resume();
      waiters = next;
    }
  }
}
複製代碼

最後,咱們須要實現 operator co_await() 方法。這隻須要構造一個 awaiter 對象。

async_manual_reset_event::awaiter
async_manual_reset_event::operator co_await() const noexcept {
  return awaiter{ *this };
}
複製代碼

咱們終於完成它了,一個可等待的異步手動重置事件,具備無鎖,無內存分配,noexcept 實現。

若是你想嘗試一下代碼,或者看看它編譯到 MSVC 和 Clang 下面的代碼,能夠看看 godbolt 上查看。

你還能夠在 cppcoro 庫中找到此類的實現,以及許多其餘有用的 awaitable 類型,例如 async_mutexasync_auto_reset_event

結語

這篇文章介紹瞭如何根據 AwaitableAwaiter 概念實現和定義運算符 co_await

它還介紹瞭如何實現一個等待的異步線程同步原語,該原語利用了在協程幀上分配 awaiter 對象的事實,以免額外的堆分配。

我但願這篇文章已經幫助你對 co_await 這個新的運算符有了更好的理解。

在下一篇博客中,我將探討 Promise 概念以及協程類型做者如何定製其協程的行爲。

致謝

我要特別感謝 Gor Nishanov 在過去幾年中耐心而熱情地回答了我關於協程的許多問題。

此外,還有 Eric Niebler 對本文的早期草稿進行審覈並提供反饋。

若是發現譯文存在錯誤或其餘須要改進的地方,歡迎到 掘金翻譯計劃 對譯文進行修改並 PR,也可得到相應獎勵積分。文章開頭的 本文永久連接 即爲本文在 GitHub 上的 MarkDown 連接。


掘金翻譯計劃 是一個翻譯優質互聯網技術文章的社區,文章來源爲 掘金 上的英文分享文章。內容覆蓋 AndroidiOS前端後端區塊鏈產品設計人工智能等領域,想要查看更多優質譯文請持續關注 掘金翻譯計劃官方微博知乎專欄

相關文章
相關標籤/搜索