使用 C++/WinRT 的進階並行與非同步

本主題描述了在 C++/WinRT 中具備並行與非同步的進階情境。

想了解這個主題,請先閱讀《 並行與非同步操作》。

將工作交由 Windows 執行緒集區處理

協程是一個與其他函式相同的函式,因為呼叫者會被阻擋,直到函式將執行回傳給它。 協程回歸的第一個機會是第一個 co_awaitco_returnco_yield

所以,在協程中執行計算限制工作之前,你需要先將執行回傳給呼叫者(換句話說,引入一個暫停點),以確保呼叫者不會被阻塞。 如果你還沒用其他操作來做 co_await-,那你可以 co_awaitwinrt::resume_background 函式。 這會將控制權還給呼叫者,然後立即在執行緒池執行緒上繼續執行。

實作中使用的執行緒池是低階 Windows 執行緒池,因此效率最高。

IAsyncOperation<uint32_t> DoWorkOnThreadPoolAsync()
{
    co_await winrt::resume_background(); // Return control; resume on thread pool.

    uint32_t result;
    for (uint32_t y = 0; y < height; ++y)
    for (uint32_t x = 0; x < width; ++x)
    {
        // Do compute-bound work here.
    }
    co_return result;
}

以執行緒親和性為考量的程式設計

這個情境是前一個情境的延伸。 你會把部分工作卸給執行緒池,但你又想在使用者介面(UI)中顯示進度。

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    co_await winrt::resume_background();
    // Do compute-bound work here.

    textblock.Text(L"Done!"); // Error: TextBlock has thread affinity.
}

上述程式碼會拋出 winrt::hresult_wrong_thread 例外狀況,因為 TextBlock 必須由建立它的執行緒,也就是 UI 執行緒,來更新。 其中一種解決方案,是擷取我們的協程最初被呼叫時所在的執行緒上下文。 要做到這一點,請先建立一個 winrt::apartment_context 物件,執行背景工作,然後 co_awaitapartment_context 以切換回呼叫端內容。

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    winrt::apartment_context ui_thread; // Capture calling context.

    co_await winrt::resume_background();
    // Do compute-bound work here.

    co_await ui_thread; // Switch back to calling context.

    textblock.Text(L"Done!"); // Ok if we really were called from the UI thread.
}

只要上述協程是從創建 TextBlock 的 UI 執行緒呼叫,這個技術就有效。 在你的應用程式中,會有很多你能確定這一點的情況。

如果你想用更通用的 UI 更新方案,當你不確定呼叫執行緒時,可以用 co_awaitwinrt::resume_foreground 函式切換到特定的前景執行緒。 在下方的程式碼範例中,我們透過傳遞與 TextBlock 相關的調度器佇列(存取其 DispatcherQueue 屬性)來指定前景執行緒。 winrt::resume_foreground 的實作會呼叫該調度者佇列物件上的 DispatcherQueue.TryEnqueue,來執行協程中後續的工作。

IAsyncAction DoWorkAsync(TextBlock textblock)
{
    co_await winrt::resume_background();
    // Do compute-bound work here.

    // Switch to the foreground thread associated with textblock.
    co_await winrt::resume_foreground(textblock.DispatcherQueue());

    textblock.Text(L"Done!"); // Guaranteed to work.
}

winrt::resume_foreground 函式會採用一個可選的優先權參數。 如果你用的是這個參數,那麼上面顯示的模式是合適的。 如果沒有,那你可以選擇簡化 co_await winrt::resume_foreground(someDispatcherObject);co_await someDispatcherObject;

協程中的執行上下文、恢復執行與切換

大致來說,在協程中達到暫停點後,原始執行緒可能會消失,且可在任何執行緒上進行恢復(換句話說,任何執行緒都可以呼叫非同步操作的 完成 方法)。

但如果你co_await使用四種 Windows 執行階段 非同步操作類型(IAsyncXxx),那麼 C++/WinRT 會擷取你所在位置co_await的呼叫上下文。 而且這樣可以確保當續集繼續時,你仍然處於那個脈絡中。 C++/WinRT 會先檢查你是否已位於呼叫內容脈絡中,若不是,就切換到該內容脈絡。 如果你在 co_await 之前位於單一執行緒 Apartment(STA)執行緒上,那麼之後你也會位於同一個執行緒上;如果你在 co_await 之前位於多執行緒 Apartment(MTA)執行緒上,那麼之後你也會位於某個 MTA 執行緒上。

IAsyncAction ProcessFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;

    // The thread context at this point is captured...
    SyndicationFeed syndicationFeed{ co_await syndicationClient.RetrieveFeedAsync(rssFeedUri) };
    // ...and is restored at this point.
}

你可以依賴這種行為的原因,是因為 C++/WinRT 提供了將這些 Windows 執行階段 非同步操作類型調整到 C++ 協程語言支援的程式碼(這些程式碼稱為等待適配器)。 C++/WinRT 中其餘的可等待類型僅是執行緒集區包裝器和/或輔助程式;因此它們會在執行緒集區上完成。

using namespace std::chrono_literals;
IAsyncOperation<int> return_123_after_5s()
{
    // No matter what the thread context is at this point...
    co_await 5s;
    // ...we're on the thread pool at this point.
    co_return 123;
}

如果你 co_await 使用其他類型——即使是在 C++/WinRT 協程實作中——那麼另一個函式庫會提供介面卡,你需要了解這些介面卡在恢復和上下文上的運作。

為了減少上下文切換,你可以使用我們本主題中已經見過的一些技巧。 讓我們看看一些如何這樣做的示例。 在下一個偽程式碼範例中,我們展示了一個事件處理程序的輪廓,該處理程序呼叫 Windows 執行階段 API 載入映像檔,然後切換到背景執行緒處理該影像,然後返回 UI 執行緒在 UI 中顯示該影像。

IAsyncAction MainPage::ClickHandler(IInspectable /* sender */, RoutedEventArgs /* args */)
{
    // We begin in the UI context.

    // Call StorageFile::OpenAsync to load an image file.

    // The call to OpenAsync occurred on a background thread, but C++/WinRT has restored us to the UI thread by this point.

    co_await winrt::resume_background();

    // We're now on a background thread.

    // Process the image.

    co_await winrt::resume_foreground(this->DispatcherQueue());

    // We're back on MainPage's UI thread.

    // Display the image in the UI.
}

在這種情況下,呼叫 StorageFile::OpenAsync 會有些低效率。 在恢復時,必須切換到背景執行緒(讓處理器能將執行回傳給呼叫者),之後 C++/WinRT 會恢復 UI 執行緒的上下文。 但在這種情況下,直到我們準備更新 UI 之前,其實不需要在 UI 執行緒裡。 我們在呼叫 winrt:::resume_background之前呼叫越多Windows 執行階段 API,就越會產生不必要的來回上下文切換。 解決方法是在此之前不要呼叫任何 Windows 執行階段 API。 把它們全部移到 winrt::resume_background 之後。

IAsyncAction MainPage::ClickHandler(IInspectable /* sender */, RoutedEventArgs /* args */)
{
    // We begin in the UI context.

    co_await winrt::resume_background();

    // We're now on a background thread.

    // Call StorageFile::OpenAsync to load an image file.

    // Process the image.

    co_await winrt::resume_foreground(this->DispatcherQueue());

    // We're back on MainPage's UI thread.

    // Display the image in the UI.
}

如果你想做更進階的操作,那麼你可以自行撰寫自己的 await 配接器。 舉例來說,如果你想讓 在 co_await 非同步動作完成的同一個執行緒上繼續執行(這樣就不會有上下文切換),那麼你可以先寫類似下面展示的等待適配器。

Note

以下範例僅供教育用途;這是讓你開始了解 AWAIT 轉接器的運作方式。 如果你想在自己的程式碼庫中使用這種技術,我們建議你自行開發並測試自己的 await 適配器結構(一個或多個)。 例如,您可以寫成 complete_on_anycomplete_on_current,以及 complete_on(dispatcher)。 也可以考慮把它們做成以 IAsyncXxx 類型作為範本參數的範本。

struct no_switch
{
    no_switch(Windows::Foundation::IAsyncAction const& async) : m_async(async)
    {
    }

    bool await_ready() const
    {
        return m_async.Status() == Windows::Foundation::AsyncStatus::Completed;
    }

    void await_suspend(std::experimental::coroutine_handle<> handle) const
    {
        m_async.Completed([handle](Windows::Foundation::IAsyncAction const& /* asyncInfo */, Windows::Foundation::AsyncStatus const& /* asyncStatus */)
        {
            handle();
        });
    }

    auto await_resume() const
    {
        return m_async.GetResults();
    }

private:
    Windows::Foundation::IAsyncAction const& m_async;
};

若要了解如何使用 no_switch await 配接器,首先必須知道,當 C++ 編譯器遇到 co_await 運算式時,會尋找名為 await_readyawait_suspendawait_resume 的函式。 C++/WinRT 函式庫提供這些函式,讓你預設能得到合理的行為,就像這樣。

IAsyncAction async{ ProcessFeedAsync() };
co_await async;

要使用 no_switch `await` 轉接器,只要將該 co_await 運算式的型別從 IAsyncXxx 改為 no_switch,如下所示。

IAsyncAction async{ ProcessFeedAsync() };
co_await static_cast<no_switch>(async);

接著,C++ 編譯器不再尋找三個符合 IAsyncXXawait_xxx 函式,而是尋找符合 no_switch 的函式。

更深入探討 winrt::resume_foreground

C++/WinRT 2.0 起,winrt::resume_foreground 函式即使是在調度器執行緒上呼叫,也會暫停(在先前的版本中,因為它只有在尚未位於調度器執行緒上時才會暫停,所以在某些情況下可能會導致死結)。

目前的行為表示你可以仰賴堆疊展開和重新排入佇列會發生;而這對系統穩定性非常重要,尤其是在低階系統程式碼中。 上方「 以執行緒親和性為念的程式設計」章節中最後列出的程式碼,說明了在背景執行緒上執行複雜的計算,然後切換到適當的 UI 執行緒以更新使用者介面(UI)。

以下是 winrt::resume_foreground 在內部的樣貌。

auto resume_foreground(...) noexcept
{
    struct awaitable
    {
        bool await_ready() const
        {
            return false; // Queue without waiting.
            // return m_dispatcher.HasThreadAccess(); // The C++/WinRT 1.0 implementation.
        }
        void await_resume() const {}
        void await_suspend(coroutine_handle<> handle) const { ... }
    };
    return awaitable{ ... };
};

這種現行與過去的行為類似於 Win32 應用程式開發中 PostMessageSendMessage 的差異。 PostMessage 會排隊處理工作,然後在不等待工作完成前解開堆疊。 堆疊拆解可能是必不可少的。

winrt::resume_foreground 函式最初支援 CoreDispatcher(綁定於 CoreWindow),該功能在 Windows 10 之前推出。 在 WinUI 3 和 Windows 應用程式 SDK 應用程式中,改用 DispatcherQueue。 你可以為自己的目的建立 DispatcherQueue 。 請考慮這個簡單的主控台應用程式。

using namespace Windows::System;

winrt::fire_and_forget RunAsync(DispatcherQueue queue);
 
int main()
{
    auto controller{ DispatcherQueueController::CreateOnDedicatedThread() };
    RunAsync(controller.DispatcherQueue());
    getchar();
}

上述範例是在私有執行緒中建立一個包含在控制器內的佇列,然後將控制器傳遞給協程。 協程可以使用此佇列在私有執行緒上進行等候(暫停並恢復)。 DispatcherQueue 另一個常見用途是在傳統桌面或 Win32 應用程式的當前 UI 執行緒中建立佇列。

DispatcherQueueController CreateDispatcherQueueController()
{
    DispatcherQueueOptions options
    {
        sizeof(DispatcherQueueOptions),
        DQTYPE_THREAD_CURRENT,
        DQTAT_COM_STA
    };
 
    ABI::Windows::System::IDispatcherQueueController* ptr{};
    winrt::check_hresult(CreateDispatcherQueueController(options, &ptr));
    return { ptr, take_ownership_from_abi };
}

這說明了你如何呼叫並整合 Win32 函式到你的 C++/WinRT 專案中,只要呼叫類似 Win32 的 CreateDispatcherQueueController 函式來建立控制器,然後將產生的佇列控制器的所有權轉移給呼叫者,作為 WinRT 物件。 這也正是你可以在現有的 Petzold 風格 Win32 桌面應用程式中,支援高效且無縫佇列處理的方法。

winrt::fire_and_forget RunAsync(DispatcherQueue queue);
 
int main()
{
    Window window;
    auto controller{ CreateDispatcherQueueController() };
    RunAsync(controller.DispatcherQueue());
    MSG message;
 
    while (GetMessage(&message, nullptr, 0, 0))
    {
        DispatchMessage(&message);
    }
}

上面,簡單的 主要 功能是從建立一個視窗開始。 你可以想像這是註冊一個視窗類別,然後呼叫 CreateWindow 來建立頂層桌面視窗。 接著會呼叫 CreateDispatcherQueueController 函式來建立該佇列控制器,然後呼叫與該控制器所擁有的調度器佇列的協程。 接著會進入傳統的訊息泵浦,協程會自然地在此執行緒上繼續執行。 完成後,你就可以回到優雅的協程世界,在應用程式內處理非同步或以訊息為基礎的工作流程。

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ... // Begin on the calling thread...
 
    co_await winrt::resume_foreground(queue);
 
    ... // ...resume on the dispatcher thread.
}

呼叫 winrt::resume_foreground 時,總是會先將作業排入佇列,然後再展開堆疊。 你也可以選擇性地設定恢復優先順序。

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await winrt::resume_foreground(queue, DispatcherQueuePriority::High);
 
    ...
}

或者,使用預設的排隊順序。

...
#include <winrt/Windows.System.h>
using namespace Windows::System;
...
winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    co_await queue;
 
    ...
}

Note

如上所示,務必包含你要 co_await-ing 的類型命名空間的投影標頭。 例如,Windows::System::DispatcherQueueMicrosoft::UI::Dispatching::DispatcherQueue

或者,在這種情況下,偵測佇列關閉並妥善處理。

winrt::fire_and_forget RunAsync(DispatcherQueue queue)
{
    ...
 
    if (co_await queue)
    {
        ... // Resume on dispatcher thread.
    }
    else
    {
        ... // Still on calling thread.
    }
}

co_await 運算式會傳回 true,表示後續會在分派器執行緒上繼續執行。 換句話說,排隊是成功的。 反之,它會回傳 false,表示執行會繼續在呼叫端執行緒上進行,因為佇列的控制器正在關閉,且不再處理佇列要求。

因此,當你將 C++/WinRT 與協程結合時,你擁有大量運算能力;尤其是在做一些老派 Petzold 風格的桌面應用程式開發時。

取消非同步操作與取消回撥

Windows 執行階段 的非同步程式設計功能允許你取消飛行中的非同步動作或操作。 這裡有一個範例,呼叫 StorageFolder::GetFilesAsync 來取得可能龐大的檔案集合,並將產生的非同步操作物件儲存在資料成員中。 使用者可以選擇取消操作。

// MainPage.xaml
...
<Button x:Name="workButton" Click="OnWork">Work</Button>
<Button x:Name="cancelButton" Click="OnCancel">Cancel</Button>
...

// MainPage.h
...
#include <winrt/Windows.Foundation.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Storage.Search.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Foundation::Collections;
using namespace Windows::Storage;
using namespace Windows::Storage::Search;
using namespace Microsoft::UI::Xaml;
...
struct MainPage : MainPageT<MainPage>
{
    MainPage()
    {
        InitializeComponent();
    }

    IAsyncAction OnWork(IInspectable /* sender */, RoutedEventArgs /* args */)
    {
        workButton().Content(winrt::box_value(L"Working..."));

        // Enable the Pictures Library capability in the app manifest file.
        StorageFolder picturesLibrary{ KnownFolders::PicturesLibrary() };

        m_async = picturesLibrary.GetFilesAsync(CommonFileQuery::OrderByDate, 0, 1000);

        IVectorView<StorageFile> filesInFolder{ co_await m_async };

        workButton().Content(box_value(L"Done!"));

        // Process the files in some way.
    }

    void OnCancel(IInspectable const& /* sender */, RoutedEventArgs const& /* args */)
    {
        if (m_async.Status() != AsyncStatus::Completed)
        {
            m_async.Cancel();
            workButton().Content(winrt::box_value(L"Canceled"));
        }
    }

private:
    IAsyncOperation<::IVectorView<StorageFile>> m_async;
};
...

關於取消的實作端,讓我們先從一個簡單的例子開始。

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncAction ImplicitCancelationAsync()
{
    while (true)
    {
        std::cout << "ImplicitCancelationAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction MainCoroutineAsync()
{
    auto implicit_cancelation{ ImplicitCancelationAsync() };
    co_await 3s;
    implicit_cancelation.Cancel();
}

int main()
{
    winrt::init_apartment();
    MainCoroutineAsync().get();
}

如果你執行上述範例,你會看到 ImplicitCancelationAsync 每秒印出一則訊息,持續三秒鐘,之後它會自動因被取消而終止。 這之所以有效,是因為遇到一個 co_await 表達式時,協程會檢查該表達式是否已被取消。 如果有,就會短路;如果沒有,那它就會正常懸浮。

當然,取消可以在協程暫停期間發生。 只有在協程恢復執行,或執行到另一個 co_await 時,才會檢查是否已取消。 問題在於響應取消要求時的延遲粒度可能過粗。

所以,另一個選項是從你的協程內部明確地輪詢取消狀態。 請用下方列表中的程式碼更新上述範例。 在這個新範例中, ExplicitCancelationAsync 會取回 winrt:::get_cancellation_token 函式回傳的物件,並定期檢查協程是否已被取消。 只要沒有被取消,協程就會無限循環;一旦被取消,迴圈與函式便會正常退出。 結果與前述相同,但此處退出是明確且受控的。

IAsyncAction ExplicitCancelationAsync()
{
    auto cancelation_token{ co_await winrt::get_cancellation_token() };

    while (!cancelation_token())
    {
        std::cout << "ExplicitCancelationAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction MainCoroutineAsync()
{
    auto explicit_cancelation{ ExplicitCancelationAsync() };
    co_await 3s;
    explicit_cancelation.Cancel();
}
...

winrt::get_cancellation_token 進行等待時,會取得一個知道協程正代表你產生的 IAsyncAction 的取消權杖。 你可以用該標記上的函式呼叫運算子查詢消去狀態——基本上就是輪詢消去。 如果你執行某種受計算限制的操作,或是遍歷一個大型集合,那麼這是合理的技術。

註冊取消回撥

Windows 執行階段 的取消不會自動流向其他非同步物件。 但是——在 Windows SDK 的 10.0.17763.0(Windows 10,版本 1809)中引入——你可以註冊取消回撥。 這是一個預先掛鉤,可藉此傳遞取消訊號,並可與現有的並發程式庫整合。

在接下來的程式碼範例中, NestedCoroutineAsync 會執行這些工作,但它沒有特殊的消去邏輯。 CancelationPropagatorAsync 本質上就是巢狀協程的包裝器;該包裝器會先行轉傳取消要求。

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncAction NestedCoroutineAsync()
{
    while (true)
    {
        std::cout << "NestedCoroutineAsync: do some work for 1 second" << std::endl;
        co_await 1s;
    }
}

IAsyncAction CancelationPropagatorAsync()
{
    auto cancelation_token{ co_await winrt::get_cancellation_token() };
    auto nested_coroutine{ NestedCoroutineAsync() };

    cancelation_token.callback([=]
    {
        nested_coroutine.Cancel();
    });

    co_await nested_coroutine;
}

IAsyncAction MainCoroutineAsync()
{
    auto cancelation_propagator{ CancelationPropagatorAsync() };
    co_await 3s;
    cancelation_propagator.Cancel();
}

int main()
{
    winrt::init_apartment();
    MainCoroutineAsync().get();
}

CancelationPropagatorAsync 會為自己的取消回呼註冊一個 lambda 函式,接著等待(暫停),直到巢狀作業完成。 當或如果 CancellationPropagatorAsync 被取消時,它會將取消傳播至巢狀協程。 取消也不需要投票;取消也不會被無限期封鎖。 這個機制夠彈性,讓你可以用它來與一個對 C++/WinRT 一無所知的協程或並行函式庫互通。

報告進度

如果你的協程回傳 IAsyncActionWithProgressIAsyncOperationWithProgress 其中之一,則你可以取得由 winrt::get_progress_token 函式傳回的物件,並使用該物件將進度回報給進度處理常式。 這裡有一個程式碼範例。

// main.cpp
#include <iostream>
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace std::chrono_literals;

IAsyncOperationWithProgress<double, double> CalcPiTo5DPs()
{
    auto progress{ co_await winrt::get_progress_token() };

    co_await 1s;
    double pi_so_far{ 3.1 };
    progress.set_result(pi_so_far);
    progress(0.2);

    co_await 1s;
    pi_so_far += 4.e-2;
    progress.set_result(pi_so_far);
    progress(0.4);

    co_await 1s;
    pi_so_far += 1.e-3;
    progress.set_result(pi_so_far);
    progress(0.6);

    co_await 1s;
    pi_so_far += 5.e-4;
    progress.set_result(pi_so_far);
    progress(0.8);

    co_await 1s;
    pi_so_far += 9.e-5;
    progress.set_result(pi_so_far);
    progress(1.0);

    co_return pi_so_far;
}

IAsyncAction DoMath()
{
    auto async_op_with_progress{ CalcPiTo5DPs() };
    async_op_with_progress.Progress([](auto const& sender, double progress)
    {
        std::wcout << L"CalcPiTo5DPs() reports progress: " << progress << L". "
                   << L"Value so far: " << sender.GetResults() << std::endl;
    });
    double pi{ co_await async_op_with_progress };
    std::wcout << L"CalcPiTo5DPs() is complete !" << std::endl;
    std::wcout << L"Pi is approx.: " << pi << std::endl;
}

int main()
{
    winrt::init_apartment();
    DoMath().get();
}

要回報進度,請以進度值為參數呼叫進度標記。 要設定臨時結果,請使用進度標記上的方法 set_result()

Note

報告臨時結果需使用 C++/WinRT 版本 2.0.210309.3 或更新版本。

上述範例選擇為每個進度報告設定一個臨時結果。 您可以選擇隨時回報暫定結果,或完全不回報。 它不必搭配進度報告。

Note

對於非同步動作或操作,實作多個 completion handler 是不正確的做法。 你可以選擇一個代表來代表完成的活動,也可以選擇 co_await 。 如果你兩者都有,第二樣就會失敗。 以下兩種補全處理程序中任一種都適用;不要同時針對同一個非同步物件同時使用。

auto async_op_with_progress{ CalcPiTo5DPs() };
async_op_with_progress.Completed([](auto const& sender, AsyncStatus /* status */)
{
    double pi{ sender.GetResults() };
});
auto async_op_with_progress{ CalcPiTo5DPs() };
double pi{ co_await async_op_with_progress };

如需更多有關完成處理常式的資訊,請參閱 非同步動作與作業的委派類型

開火後就忘了

有時候,你有一項任務可以和其他工作同時進行,你不需要等該任務完成(因為沒有其他工作依賴它),也不需要它回傳一個值。 在那種情況下,你只要啟動這項工作,之後就不用管了。 你可以藉由撰寫一個回傳型別為 winrt::fire_and_forget 的協同程式來做到這一點(而不是使用 Windows 執行階段 非同步作業類型之一,或 concurrency::task)。

// main.cpp
#include <winrt/Windows.Foundation.h>

using namespace winrt;
using namespace std::chrono_literals;

winrt::fire_and_forget CompleteInFiveSeconds()
{
    co_await 5s;
}

int main()
{
    winrt::init_apartment();
    CompleteInFiveSeconds();
    // Do other work here.
}

Winrt::fire_and_forget 也適合作為事件處理程序的回傳類型,方便你在事件處理程序中執行非同步操作。 這裡有一個例子(也請參見 C++/WinRT 中的強參考與弱參考)。

winrt::fire_and_forget MyClass::MyMediaBinder_OnBinding(MediaBinder const&, MediaBindingEventArgs args)
{
    auto lifetime{ get_strong() }; // Prevent *this* from prematurely being destructed.
    auto ensure_completion{ unique_deferral(args.GetDeferral()) }; // Take a deferral, and ensure that we complete it.

    auto file{ co_await StorageFile::GetFileFromApplicationUriAsync(Uri(L"ms-appx:///video_file.mp4")) };
    args.SetStorageFile(file);

    // The destructor of unique_deferral completes the deferral here.
}

第一個參數( 發送者)未被命名,因為我們從未使用過它。 因此我們可以放心把它當作參考。 但請注意, args 是透過值傳遞的。 請參見上方的 參數傳遞 段落。

正在等待核心控制代碼

C++/WinRT 提供 winrt::resume_on_signal 函式,你可以用它暫停,直到收到核心事件的訊號。 你有責任確保帳號在你 co_await resume_on_signal(h) 退貨前保持有效。 resume_on_signal 本身無法幫你做到這點,因為你可能在 resume_on_signal 開始前就已經失去了把手,就像這個第一個例子一樣。

IAsyncAction Async(HANDLE event)
{
    co_await DoWorkAsync();
    co_await resume_on_signal(event); // The incoming handle is not valid here.
}

傳入的 HANDLE 只在函式返回前有效,而此函式(它是協同程式)會在第一個暫止點返回(在此情況下是第一個 co_await)。 在等待 DoWorkAsync 的同時,控制權已回到呼叫者手中,呼叫框架已超出範圍,且你不再知道當協程恢復時,handle 是否有效。

技術上來說,我們的協程是以值傳遞方式接收其參數,這本來就應如此(參見上文的 參數傳遞)。 但在這個案例中,我們需要更進一步,遵循那個指引的 精神 (而不只是字面意思)。 我們需要連同帳號一起傳遞一個強烈的參考(換句話說,就是所有權)。 方法如下。

IAsyncAction Async(winrt::handle event)
{
    co_await DoWorkAsync();
    co_await resume_on_signal(event); // The incoming handle *is* valid here.
}

以值傳遞 winrt::handle 提供所有權語意,確保核心 handle 在協程的整個生命週期內都有效。

你可以這樣稱呼這個協程。

namespace
{
    winrt::handle duplicate(winrt::handle const& other, DWORD access)
    {
        winrt::handle result;
        if (other)
        {
            winrt::check_bool(::DuplicateHandle(::GetCurrentProcess(),
		        other.get(), ::GetCurrentProcess(), result.put(), access, FALSE, 0));
        }
        return result;
    }

    winrt::handle make_manual_reset_event(bool initialState = false)
    {
        winrt::handle event{ ::CreateEvent(nullptr, true, initialState, nullptr) };
        winrt::check_bool(static_cast<bool>(event));
        return event;
    }
}

IAsyncAction SampleCaller()
{
    handle event{ make_manual_reset_event() };
    auto async{ Async(duplicate(event)) };

    ::SetEvent(event.get());
    event.close(); // Our handle is closed, but Async still has a valid handle.

    co_await async; // Will wake up when *event* is signaled.
}

你可以像這個例子一樣,將逾時值傳給 resume_on_signal

winrt::handle event = ...

if (co_await winrt::resume_on_signal(event.get(), std::literals::2s))
{
    puts("signaled");
}
else
{
    puts("timed out");
}

讓非同步逾時變簡單

C++/WinRT 在 C++ 協程上投入大量資源。 它們對撰寫並行程式碼的影響是轉化性的。 本節討論非同步細節不重要,且你只想當下得到結果的情況。 因此,C++/WinRT 對 IAsyncAction 的 Windows 執行階段 非同步操作介面實作有一個 get 函式,類似於 std::future 所提供的。

using namespace winrt::Windows::Foundation;
int main()
{
    IAsyncAction async = ...
    async.get();
    puts("Done!");
}

get 函式會無限期阻塞,而非同步物件則會完成作業。 非同步物件通常壽命很短,所以這通常就足夠了。

但有些情況這還不夠,過了一段時間後你就得放棄等待。 寫這些程式碼一直都是可行的,這要歸功於 Windows 執行階段 提供的建構模組。 但現在 C++/WinRT 提供了 wait_for 函式,讓這件事變得簡單許多。 它也是在 IAsyncAction 上實作,且同樣類似 std::future 提供的版本。

using namespace std::chrono_literals;
int main()
{
    IAsyncAction async = ...
 
    if (async.wait_for(5s) == AsyncStatus::Completed)
    {
        puts("done");
    }
}

Note

wait_for 在介面上使用 std::chrono::d uration ,但其範圍比 std::chrono::d uration 還小(約 49.7 天)。

下一個例子中的 wait_for 會等待約五秒鐘,然後檢查完成。 如果比較結果是有利的,你就知道非同步物件成功完成,完成了。 如果你在等待某個結果,可以直接呼叫 GetResults 方法來取得結果。

Note

wait_forget 是互斥的(你不能同時稱它們為兩者)。 它們各自算作一個等待器,而 Windows 執行階段 的非同步動作/操作只支援一個等待器。

int main()
{
    IAsyncOperation<int> async = ...
 
    if (async.wait_for(5s) == AsyncStatus::Completed)
    {
        printf("result %d\n", async.GetResults());
    }
}

由於非同步物件在此時已完成, GetResults 方法會立即回傳結果,無需再等待。 如你所見, wait_for 會回傳非同步物件的狀態。 所以你可以用它來做更細緻的控制,就像這樣。

switch (async.wait_for(5s))
{
case AsyncStatus::Completed:
    printf("result %d\n", async.GetResults());
    break;
case AsyncStatus::Canceled:
    puts("canceled");
    break;
case AsyncStatus::Error:
    puts("failed");
    break;
case AsyncStatus::Started:
    puts("still running");
    break;
}
  • 請記得 AsyncStatus::Completed 表示非同步物件已成功完成,你可以呼叫 GetResults 方法來取得任何結果。
  • AsyncStatus::Canceled 表示非同步物件已被取消。 取消通常是由來電者申請,因此處理這種狀態的情況較為罕見。 通常,取消的非同步物件會直接丟棄。 如果你願意,可以呼叫 GetResults 方法重新拋出取消例外。
  • AsyncStatus::Error 表示非同步物件以某種方式失敗。 如果你願意,可以呼叫 GetResults 方法來重新拋出該例外。
  • AsyncStatus::Started 表示非同步物件仍在執行。 Windows 執行階段 非同步模式不允許多次等待,也不允許等待者。 這表示你不能在循環中呼叫 wait_for 。 如果等待時間已經有效逾時,那你就剩下幾個選擇。 你可以放棄該物件,或是在呼叫 GetResults 方法取得任何結果前先輪詢其狀態。 但此時最好直接丟棄這個物件。

另一種模式是只檢查 Start,其他情況則由 GetResults 處理。

if (async.wait_for(5s) == AsyncStatus::Started)
{
    puts("timed out");
}
else
{
    // will throw appropriate exception if in canceled or error state
    auto results = async.GetResults();
}

以非同步方式傳回陣列

以下是一個 MIDL 3.0 的範例,會產生 錯誤MIDL2025:[訊息]語法錯誤 [上下文]:預期 > 或接近「[」

Windows.Foundation.IAsyncOperation<Int32[]> RetrieveArrayAsync();

原因是,在參數化介面中使用陣列作為型別引數是無效的。 因此,我們需要一種不那麼明顯的方法,來達成從執行時類別方法非同步傳回陣列的目標。

你可以回傳那個被封在 PropertyValue 物件裡的陣列。 接著,呼叫端程式碼會將它拆箱。 這裡有一個程式碼範例,你可以試試看,方法是把 SampleComponent 執行時類別加入 Windows 執行階段 Component(C++/WinRT)專案,然後從(例如)Blank App, Packaged(桌面版的 WinUI 3)專案中取用這個類別。

// SampleComponent.idl
namespace MyComponentProject
{
    runtimeclass SampleComponent
    {
        Windows.Foundation.IAsyncOperation<IInspectable> RetrieveCollectionAsync();
    };
}

// SampleComponent.h
...
struct SampleComponent : SampleComponentT<SampleComponent>
{
    ...
    Windows::Foundation::IAsyncOperation<Windows::Foundation::IInspectable> RetrieveCollectionAsync()
    {
        co_return Windows::Foundation::PropertyValue::CreateInt32Array({ 99, 101 }); // Box an array into a PropertyValue.
    }
}
...

// SampleCoreApp.cpp
...
MyComponentProject::SampleComponent m_sample_component;
...
auto boxed_array{ co_await m_sample_component.RetrieveCollectionAsync() };
auto property_value{ boxed_array.as<winrt::Windows::Foundation::IPropertyValue>() };
winrt::com_array<int32_t> my_array;
property_value.GetInt32Array(my_array); // Unbox back into an array.
...

重要 API