跳至內容

C++/coroutine

維基教科書,自由的教學讀本
< C++

C++20引入了協程(coroutine)。協程是函數的一種泛化,它允許函數被掛起,然後在稍後被恢復執行。協程在「調用」和「返回」操作外,還有3種額外的操作:「掛起」、「恢復」、「摧毀」。[1]

  • 掛起(Suspend)操作是協程執行到co_awaitco_yield關鍵字處(稱為掛起點),掛起自身的執行,把執行轉交給協程的(最初的)調用者或者(中途的)恢復者。掛起操作首先準備堆上的協程幀(coroutine frame),包括寫入是哪個掛起點以便將來作為恢復秩序的地址。如果要把執行轉交給協程的調用者或者恢復者,則協程的棧幀被彈出運行站。
  • 恢復(Resume)操作:當一個函數要恢復一個已經被掛起的協程,它會調用協程幀(coroutine frame)中void resume()方法。resume()將分配協程的棧幀、把調用者的返回地址保存在棧幀中,然後轉移到協程的上次掛起點繼續執行。當協程再次被掛起或者執行完畢, resume()的調用者將恢復秩序。
  • 摧毀(Destroy)操作將摧毀一個掛起的協程。首先它類似於恢復操作將恢復協程的棧幀、保存摧毀操作調用者的返回地址。然後調用調用協程幀(coroutine frame)中void destroy()方法,調用所有作用域內的局部變量的析構函數,最後釋放堆上的協程幀。
  • 調用(Call)操作:從調用者角度,調用一個協程和調用普通函數並無區別。但協程執行到第一個掛起點後,將恢復調用者的執行。協程被調用後,首先在堆上創建協程幀,從棧幀複製/移動參數到協程幀,這些參數的生命期需要跨越第一個掛起點。
  • 返回(Return)操作:與普通函數的返回略有不同。協程執行co_return時,在(可定製的)某處存儲返回值,然後析構作用域內的局部變量(不含參數)。然後協程可執行一些可定製邏輯。最後協程掛起操作或者摧毀操作,把執行交給調用者或者恢復者。需要注意的是,傳遞給返回操作的返回值與從調用操作返回的返回值並不相同,因為返回操作可能在調用者從最初的調用操作恢復後很長時間才執行。

頭文件<coroutine>提供了 3個新的C++關鍵字: co_await, co_yield and co_return 新的類型:

coroutine_handle

coroutine_traits<Ts...> suspend_always

Promise機制

[編輯]

可以認為協程是一個狀態機。協程的promise對象可看作「協程狀態控制器」對象,控制了協程的行為,可用於跟蹤協程的狀態。

協程執行到某一點時,在協程幀中創建協程的promise對象。編譯器產生promise對象的若干方法。協程的執行大致為:

{
  co_await promise.initial_suspend();
  try
  {
    <body-statements>
  }
  catch (...)
  {
    promise.unhandled_exception();
  }
FinalSuspend:
  co_await promise.final_suspend();
}

調用協程時初始化要執行:

  1. 使用運算符new分配一個協程幀(可選)。
  2. 將任何函數參數複製到協程幀中。如果參數是通過值傳遞給協程的,那麼這些參數將通過調用類型的移動構造函數複製到協程幀中。如果參數是通過引用(無論是左值還是右值)傳遞給協程的,那麼只有引用被複製到協程幀中,而不是它們指向的值。在將參數通過引用傳遞到協程時,涉及許多陷阱,因為協程的生命周期內,引用不一定始終保持有效。許多通常用於普通函數的技術,如完美轉發和通用引用,如果用於協程,可能會導致代碼具有未定義行為。
  3. 調用類型為P的promise對象的構造函數。
  4. 調用promise.get_return_object()方法以獲取當協程首次掛起時返回給調用者的結果。將結果保存為局部變量。
  5. 調用promise.initial_suspend()方法並使用co_await等待結果。這給了編程者一個機會在執行協程體之前先掛起執行。initial_suspend()方法要麼返回std::suspend_always(如果操作是延遲啟動的),要麼返回std::suspend_never(如果操作是立即啟動的),這兩種都是 noexcept 的可等待對象
  6. 當co_await promise.initial_suspend()表達式恢復執行時(無論是立即還是異步),協程就開始執行你編寫的協程主體語句

當執行到達一個 co_return 語句時(協程體執行完畢時,當作有一個co_return):

  1. 調用 promise.return_void() 或 promise.return_value(<expr>)。
  2. 以它們被創建的相反順序銷毀所有具有自動存儲期限的變量。
  3. 調用 promise.final_suspend() 並使用 co_await 等待結果。

當執行由於未處理的異常而離開 <body-statements> 時:

  1. 捕獲異常:在 catch 塊內調用 promise.unhandled_exception()。典型是用std::current_exception()捕獲異常。
  2. 調用 promise.final_suspend() 並使用 co_await 等待結果。因此有機會發佈結果、發出完成信號或恢復繼續執行。它還允許協程在執行運行完成並且協程幀被銷毀之前選擇性地立即掛起。請注意,對處於 final_suspend 點的協程調用 resume() 是未定義行為。你唯一能對掛起在這裏的協程做的就是銷毀它。

一旦執行離開了協程體,那麼協程幀就會被銷毀。銷毀協程幀涉及以下步驟:

  1. 調用promise對象的析構函數。
  2. 調用函數參數副本的析構函數。
  3. 使用操作符delete釋放協程幀所使用的內存(可選)。
  4. 將執行轉回調用者/恢復者。

當執行首次到達 co_await 表達式內的 <return-to-caller-or-resumer> 點,或者如果協程在沒有遇到 <return-to-caller-or-resumer> 點的情況下運行完畢,那麼協程要麼被掛起要麼被銷毀,而之前從調用 promise.get_return_object() 返回的返回對象隨後被返回給協程的調用者。

參考文獻

[編輯]
  1. Asymmetric Transfer