天天看點

VS2019中C++20的協程實作

在Visual Studio 2019 v16.8中,我們在一篇文章中宣布了對協程的支援。從那個時候開始,我們引入了一些列和協程相關的新特性和改進。今天的這篇文章,我們将這些新東西來一個彙總給大家看看,所有這些新東西都已經在Visual Studio 2019 v16.11版本中可用。

調試改進

從VS2019 v16.9版開始,單步進入(Step into)調試到一個協程的時候,執行流會直接進入到協程代碼體中(有個一個特殊情況,即這個協程一開始就被設定成了暫停執行狀态,在這種特殊情況下,執行流會直接跳過”Step over”)。跳過一個co_await将進入co_await後面的邏輯語句中,用于協程,這可能在一個完全不同的執行上下文中(甚至另一個線程)!

這允許單步執行協程,實作無縫比對應用程式的邏輯流并跳過中間的實作細節。為獲得最佳調試體驗,應将協程狀态的實作細節标記為非使用者代碼。單步執行協程現在還會在本地變量”Locals”視窗中按預期顯示函數參數,以便你可以檢視應用程式的狀态,類似于單步執行同步函數。

我們通過對标準協程的調試可視化工具的一些改進,現在可以更輕松地檢視已暫停執行協程的狀态。 之前舊版本的coroutine_handle可視化器可以顯示初始和最終暫停點的特殊名額,但僅顯示其他暫停點的數字。這個數字并不總是很容易映射回原始協程中的特定點。可視化器還顯示了協程的名稱,但隻是作為由實作生成的修改後的内部名稱,沒有對應的簽名資訊。如下圖所示:

VS2019中C++20的協程實作

我們可以看到,在上圖中,協程的名稱被顯示為:”sample_coroutine$_ResumeCoro$1(void)”。

使用Visual Studio 2019 v16.10中引入的新協程句柄可視化器,函數名稱可以被正确地顯示了。并且包含了完整的簽名資訊以幫助區分重載的協程。除了初始和最終挂起之外的挂起點資訊,可視化器中還包括了源代碼行号,以便于查找,如下圖所示:

VS2019中C++20的協程實作

在上圖中,我們可以看到,現在的協程被顯示為”sample_coroutine(int)”,并辨別其目前狀态為已挂起狀态,位于源代碼的第35行。

編譯器開關:/await:strict

在之前的一篇文章中,我們提到Legacy模式的await存在一些問題,以及在/std:c++latest中保持 /await開關與C++20協程支援不同的基本原理。Legacy模式對于早期采用C++協程但不是标準協程的使用者很有用。

/await這個編譯器開關不僅早于我們的/std:c++latest和/std:c++20這兩個開關,還早于 /std:c++17。早在協程成為C++标準的一部分之前,一些開發者就開始使用協程了。這些開發者可以使用協程,而不要求他們的代碼符合C++20甚至是C++17标準。由于标準協程僅在C++20和最新模式下可用,協程的早期采用者無法将其代碼移動到更新的語言版本,是以被困在等待協程的遺留實作中。他們無法利用一些新功能,例如對稱傳輸和改進的調試器支援,即使他們願意對協程本身進行代碼修改以使其符合C++20标準。

從Visual Studio 2019 v16.10版開始,我們引入了一個新開關,以幫助早期協程采用者過渡到合規協程并使用标準協程中的所有可用功能,這個開關就是:/await:strict。

使用此開關代替/await可以啟用與标準模式相同的C++20協程支援,但沒有/std:c++20的所有其他要求。這包括對所有标準C++20協程功能和調試器內建的支援,并禁用/await下仍然支援的所有遺留擴充/std:c++20協程和/await:strict之間的唯一差別是:後者沒有為std::coroutine_handle 定義飛船操作符(spaceship operator)。不同的是,它定義了單獨的關系運算符。

如果你的代碼依賴于C++20中未采用的擴充,則從/await遷移到/await:strict可能需要修改源代碼。與标準模式一樣,它使用頭檔案頭和std命名空間,是以你的代碼将為C++20做好準備。使用/await:strict編譯的代碼使用與/std:c++latest相同的協程ABI,是以協程對象在兩種模式之間可以保持相容。

我們鼓勵/await的所有使用者遷移到/await:strict。 你可以利用所有新的協程功能,并確定協程代碼在你可以移動到正式支援協程的C++語言版本時為C++20做好準備。 我們希望在未來的某個時候棄用和徹底地删除/await開關。

穩定性改進

Visual Studio 2019 v16.11版本中還包括幾個重要的修複程式,以提高協程的穩定性和可靠性。

最大的變化與優化器如何進行所謂的”提升”有關,這是一種決定哪些變量被放置在協程架構上以及哪些變量保留在(傳統)堆棧上的算法。許多協程錯誤可以追溯到此處的錯誤決策。通常,這表現為崩潰,或者在協程恢複執行後變量具有不正确或随機的值。此算法已被重寫為更準确,結果是更少的崩潰和更小的協程幀大小。舊算法仍然可以通過将/d2CoroNewPromotion-傳遞給cl.exe來通路。

一個相關的修複涉及如何存儲異常對象。異常的生命周期規則可能會變得複雜,需要在決定變量提升時專門處理它們。

發現并修複了與協程中的catch塊相關的錯誤。在某些情況下(即,當try塊中唯一的抛出調用來自使用者定義的awaiter方法時),優化器可能會錯誤地斷定catch塊已死,并錯誤地将其删除。編譯器現在知道awaiter方法可以抛出。

最後,解決了一個與調用析構函數的方式和時間有關的嚴重問題。這涉及如何在協程中跟蹤某些對象的構造狀态,這些對象在離開作用域時有條件地銷毀。在使用條件(三元)運算符構造對象時,它最常出現。該錯誤表現為此類臨時對象的析構函數未被調用,或者在某些情況下被調用兩次。這也已在 v16.11版本中修複。

總結

我要是有空,我就試試這個新的開關:/await:strict。異步操作确實比較吸引我。

最後

最近我寫了個東西

繼續閱讀