Appearance
C++ 協程如何共用同一個 IAsyncOperation 結果?實作技巧第三篇
原文連結:https://devblogs.microsoft.com/oldnewthing/20260529-00/?p=112368
文章說明
這是 Raymond Chen「The Old New Thing」系列的第三篇協程技術文,延續前兩篇探討如何在 C++/WinRT 中讓多個協程共用同一個 IAsyncOperation 結果。本篇聚焦於同時快取成功與失敗結果,確保內部非同步操作只執行一次,無論成功或失敗。
內容介紹
問題背景(承接前文):
前兩篇實作了基本的結果快取,但只快取成功結果。如果內部協程失敗,下次調用會重新嘗試。這在某些場景不理想——如果我們想確保內部操作只執行一次,失敗也應該被快取並在後續調用中重新拋出。
三狀態設計:
現在需要追蹤三種狀態:
- 從未嘗試(Never tried)
- 嘗試成功(Tried with success,快取結果)
- 嘗試失敗(Tried with failure,快取例外)
方案一:使用 std::variant
用 std::variant<std::monostate, winrt::Thing, std::exception_ptr> 表示三種狀態:
cpp
struct Widget : WidgetT<Widget> {
std::variant<std::monostate, winrt::Thing, std::exception_ptr> m_thing;
wil::unique_event m_busy{wil::EventOptions::Signaled};
IAsyncOperation<winrt::Thing> GetThingAsync() {
auto lifetime = get_strong();
co_await winrt::resume_on_signal(m_busy.get());
auto not_busy = m_busy.SetEvent_scope_exit();
// 如果從未嘗試,現在是機會
if (m_thing.holds_alternative<std::monostate>()) {
try {
m_thing = co_await GetThingWorkerAsync();
} catch (...) {
m_thing = std::current_exception();
}
}
// 返回快取結果或重新拋出快取的例外
if (auto thing = std::get_if<winrt::Thing>(&m_thing)) {
co_return *thing;
} else {
std::rethrow_exception(std::get<std::exception_ptr>());
}
}
};方案二:利用 nullptr 作為「未嘗試」標記
如果已知 GetThingWorkerAsync() 成功時永遠不會返回 nullptr,可以簡化為兩個變數:
cpp
struct Widget : WidgetT<Widget> {
winrt::Thing m_thing{nullptr};
std::exception_ptr m_ex;
wil::unique_event m_busy{wil::EventOptions::Signaled};
IAsyncOperation<winrt::Thing> GetThingAsync() {
auto lifetime = get_strong();
co_await winrt::resume_on_signal(m_busy.get());
auto not_busy = m_busy.SetEvent_scope_exit();
// 如果兩者都是 null,表示從未嘗試
if (!m_thing && !m_ex) {
try {
m_thing = co_await GetThingWorkerAsync();
assert(m_thing); // 確保不是 nullptr
} catch (...) {
m_ex = std::current_exception();
}
}
// 返回結果或拋出例外
if (m_thing) {
co_return m_thing;
} else {
std::rethrow_exception(m_ex);
}
}
};設計要點:
- 並行控制:使用
wil::unique_event確保同一時間只有一個協程執行內部操作 - 例外安全:用
std::exception_ptr快取例外,後續調用可重新拋出相同錯誤 - 語義清晰:
std::variant明確表達三種狀態;nullptr方案更簡潔但需要額外假設
Raymond 的補充:
文末提到之前寫過的 std::monostate 用途文章:What's the point of std::monostate?,解釋為何 variant 的第一個類型是空類型時仍有意義。
你可以帶走的重點
快取策略要考慮失敗情境:不只快取成功結果,失敗也應快取,避免重複執行註定失敗的操作。
std::variant是狀態機的好工具:明確建模「未嘗試/成功/失敗」三種互斥狀態,比布林旗標更清晰。std::exception_ptr是跨協程傳遞例外的標準做法:捕獲例外後可在其他協程重新拋出,保持錯誤資訊完整。並行控制不可省略:即使快取結果,仍需確保多個協程不會同時執行內部操作(用 event 或 mutex)。
語義 vs 效能的權衡:
std::variant更安全,nullptr方案更輕量,選擇取決於專案需求與維護性考量。
適合誰閱讀
- Windows Runtime (WinRT) 與 C++/WinRT 開發者
- 需要實作複雜非同步協程模式的系統工程師
- 學習協程狀態管理與例外處理的 C++ 進階開發者
- 關注 Raymond Chen 技術寫作風格的讀者(精準、實用、有深度)