Skip to content

C++ 協程如何共用同一個 IAsyncOperation 結果?實作技巧第三篇

原文連結:https://devblogs.microsoft.com/oldnewthing/20260529-00/?p=112368

文章說明

這是 Raymond Chen「The Old New Thing」系列的第三篇協程技術文,延續前兩篇探討如何在 C++/WinRT 中讓多個協程共用同一個 IAsyncOperation 結果。本篇聚焦於同時快取成功與失敗結果,確保內部非同步操作只執行一次,無論成功或失敗。

內容介紹

問題背景(承接前文)

前兩篇實作了基本的結果快取,但只快取成功結果。如果內部協程失敗,下次調用會重新嘗試。這在某些場景不理想——如果我們想確保內部操作只執行一次,失敗也應該被快取並在後續調用中重新拋出。

三狀態設計

現在需要追蹤三種狀態:

  1. 從未嘗試(Never tried)
  2. 嘗試成功(Tried with success,快取結果)
  3. 嘗試失敗(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);
        }
    }
};

設計要點

  1. 並行控制:使用 wil::unique_event 確保同一時間只有一個協程執行內部操作
  2. 例外安全:用 std::exception_ptr 快取例外,後續調用可重新拋出相同錯誤
  3. 語義清晰std::variant 明確表達三種狀態;nullptr 方案更簡潔但需要額外假設

Raymond 的補充

文末提到之前寫過的 std::monostate 用途文章:What's the point of std::monostate?,解釋為何 variant 的第一個類型是空類型時仍有意義。

你可以帶走的重點

  1. 快取策略要考慮失敗情境:不只快取成功結果,失敗也應快取,避免重複執行註定失敗的操作。

  2. std::variant 是狀態機的好工具:明確建模「未嘗試/成功/失敗」三種互斥狀態,比布林旗標更清晰。

  3. std::exception_ptr 是跨協程傳遞例外的標準做法:捕獲例外後可在其他協程重新拋出,保持錯誤資訊完整。

  4. 並行控制不可省略:即使快取結果,仍需確保多個協程不會同時執行內部操作(用 event 或 mutex)。

  5. 語義 vs 效能的權衡std::variant 更安全,nullptr 方案更輕量,選擇取決於專案需求與維護性考量。

適合誰閱讀

  • Windows Runtime (WinRT) 與 C++/WinRT 開發者
  • 需要實作複雜非同步協程模式的系統工程師
  • 學習協程狀態管理與例外處理的 C++ 進階開發者
  • 關注 Raymond Chen 技術寫作風格的讀者(精準、實用、有深度)

由 Wo9Fei 製作