C++/WinRT 的並行與非同步操作

這很重要

本主題介紹 協同程式co_await 的概念,我們建議您在 UI 非 UI 應用程式中都使用這些概念。 為了簡化,本入門主題中的大多數程式碼範例展示了 Windows 主控台應用程式(C++/WinRT)專案。 本主題後面的程式碼範例確實使用協程,但為了方便起見,主控台應用程式範例在退出前仍會使用阻塞性的 get 函式呼叫,避免應用程式在完成輸出前退出。 你不會在 UI 執行緒中這樣做(呼叫會封鎖的 get 函式)。 相反地,你會使用陳述 co_await 。 你在 UI 應用程式中使用的技術,詳見「 進階並行與非同步」主題。

這個入門主題展示了你可以用 C++/WinRT 建立並使用 Windows 執行階段 非同步物件的一些方式。 讀完這個主題後,特別是你在 UI 應用中會用到的技巧,也可以參考 進階並發與非同步

非同步操作與 Windows 執行階段「非同步」函式

任何可能耗時超過 50 毫秒完成的 Windows 執行階段 API 皆以非同步函式實作(名稱結尾為「Async」)。 非同步函式的實作會在另一個執行緒啟動工作,並立即返回一個代表非同步操作的物件。 當非同步操作完成時,回傳的物件會包含該工作所產生的任何值。 Windows::Foundation Windows 執行階段 命名空間包含四種非同步操作物件類型。

這些非同步操作類型會投影到對應的 winrt::Windows::Foundation C++/WinRT 命名空間中。 C++/WinRT 也包含一個內部的 await 配接器結構體。 你不會直接使用它,但多虧了這個結構體,你可以寫一個 co_await 陳述式,合作等待任何回傳這些非同步操作類型的函式結果。 你也可以自行編寫回傳這些類型的協程。

非同步 Windows 函式的範例是 SyndicationClient::RetrieveFeedAsync,它回傳一個類型為 IAsyncOperationWithProgress<TResult、TProgress> 的非同步操作物件。

讓我們來看看使用 C++/WinRT 呼叫這類 API 的幾種方式——先看封鎖式,再看非封鎖式。 為了說明基本概念,接下來幾個程式碼範例中我們將使用 Windows 控制台應用程式(C++/WinRT)專案。 更適合 UI 應用的技術則在 進階並行與非同步中討論。

封鎖呼叫執行緒

以下程式碼範例接收來自 RetrieveFeedAsync 的非同步操作物件,並呼叫該物件上的 get 來阻擋呼叫執行緒,直到非同步操作的結果可用。

如果你想直接複製貼上這個範例到 Windows 控制台應用程式(C++/WinRT)專案的主原始碼檔案,請先在專案屬性中設定「不使用預編譯標頭」。

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

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void ProcessFeed()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    SyndicationFeed syndicationFeed{ syndicationClient.RetrieveFeedAsync(rssFeedUri).get() };
    // use syndicationFeed.
}

int main()
{
    winrt::init_apartment();
    ProcessFeed();
}

呼叫 get 可讓撰寫程式碼更方便,而且特別適合用於主控台應用程式或背景執行緒,因為在這些情況下,你可能基於某些原因不想使用協程。 但它既非並行,也非非同步,因此不適合用於 UI 執行緒(而且如果你嘗試在 UI 執行緒上使用它,未最佳化的建置中會觸發斷言)。 為了避免作業系統執行緒無法完成其他有用工作,我們需要不同的技術。

撰寫協程

C++/WinRT 將 C++ 協程整合進程式模型,提供一種自然的協作等待結果的方式。 你可以透過撰寫協程來產生自己的 Windows 執行階段 非同步操作。 在下方的程式碼範例中, ProcessFeedAsync 是協程。

Note

get 函式存在於 C++/WinRT 投影類型 winrt::Windows::Foundation::IAsyncAction,因此你可以在任何 C++/WinRT 專案中呼叫該函式。 你不會在 IAsyncAction 介面中找到該函式,因為 get 並非實際 Windows 執行階段 類型 IAsyncAction 的應用程式二進位介面(ABI)表面的一部分。

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

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void PrintFeed(SyndicationFeed const& syndicationFeed)
{
    for (SyndicationItem const& syndicationItem : syndicationFeed.Items())
    {
        std::wcout << syndicationItem.Title().Text().c_str() << std::endl;
    }
}

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

int main()
{
    winrt::init_apartment();

    auto processOp{ ProcessFeedAsync() };
    // do other work while the feed is being printed.
    processOp.get(); // no more work to do; call get() so that we see the printout before the application exits.
}

協程是一種可以暫停並恢復的功能。 在上述的 ProcessFeedAsync 協程中,當執行到 co_await 陳述式時,該協程會非同步發起對 RetrieveFeedAsync 的呼叫,接著隨即暫停自身,並將控制權交還給呼叫端(在上述範例中即為 main)。 main 可以在擷取並列印摘要的同時,繼續執行其他工作。 當完成此操作( RetrieveFeedAsync 呼叫完成時), ProcessFeedAsync 協程會在下一個語句繼續執行。

你可以將一個協程整合到其他協程中。 或者你可以呼叫 get 來阻塞並等待它完成(如果有結果,也會取得結果)。 或者你可以把它傳給支援 Windows 執行階段 的其他程式語言。

也可以透過代理處理非同步動作和操作的完成及/或進度事件。 如需詳細資訊和程式碼範例,請參閱 非同步動作和作業的委派型別

如你所見,在上面的程式碼範例中,我們在退出 main 之前,仍然使用阻塞式的 get 函式呼叫。 但這只是為了避免應用程式在列印完輸出前退出。

以非同步方式傳回 Windows 執行階段 類型

在接下來的範例中,我們會對特定 URI 的 RetrieveFeedAsync 進行封裝呼叫,給我們一個 RetrieveBlogFeedAsync 函式,該函式會非同步回傳 SyndicationFeed。

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

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

void PrintFeed(SyndicationFeed const& syndicationFeed)
{
    for (SyndicationItem const& syndicationItem : syndicationFeed.Items())
    {
        std::wcout << syndicationItem.Title().Text().c_str() << std::endl;
    }
}

IAsyncOperationWithProgress<SyndicationFeed, RetrievalProgress> RetrieveBlogFeedAsync()
{
    Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
    SyndicationClient syndicationClient;
    return syndicationClient.RetrieveFeedAsync(rssFeedUri);
}

int main()
{
    winrt::init_apartment();

    auto feedOp{ RetrieveBlogFeedAsync() };
    // do other work.
    PrintFeed(feedOp.get());
}

在上述範例中,RetrieveBlogFeedAsync 會傳回 IAsyncOperationWithProgress,其同時具有進度和傳回值。 當 RetrieveBlogFeedAsync 正在執行其作業並擷取摘要時,我們可以同時做其他工作。 接著,我們呼叫該非同步操作物件的 get 來阻塞,等待它完成,然後取得操作結果。

如果您要以非同步方式傳回 Windows 執行階段 類型,則應傳回 IAsyncOperation<TResult>IAsyncOperationWithProgress<TResult, TProgress>。 任何第一方或第三方執行階段類別都算符合條件,或者任何可傳遞給 Windows 執行階段 函式或從中傳回的類型也算符合條件(例如 intwinrt::hstring)。 如果你嘗試用非 Windows 執行階段 類型使用這些非同步操作類型,編譯器會協助你解決「T 必須是 WinRT 類型」的錯誤。

如果協程至少沒有一個 co_await 陳述式,那麼要符合協程資格,它必須至少有一個 co_return 或一個 co_yield 陳述式。 在某些情況下,你的協同程式可以在不引入任何非同步作業的情況下回傳值,因此既不會發生阻塞,也不需要切換執行脈絡。 這裡有一個範例,可藉由快取某個值來達成這點(在第二次及後續呼叫時)。

winrt::hstring m_cache;

IAsyncOperation<winrt::hstring> ReadAsync()
{
    if (m_cache.empty())
    {
        // Asynchronously download and cache the string.
    }
    co_return m_cache;
}

非同步傳回非 Windows-Runtime 類型

如果你以非同步方式傳回的型別不是 Windows 執行階段 型別,則應傳回平行模式程式庫(PPL)的 concurrency::task。 我們推薦 concurrency::task ,因為它比 std::future 提供更好的效能(以及未來更好的相容性)。

Tip

如果你包含 <pplawait.h>,那麼你可以使用 concurrency::task 作為協程型態。

// main.cpp
#include <iostream>
#include <ppltasks.h>
#include <winrt/Windows.Foundation.Collections.h>
#include <winrt/Windows.Web.Syndication.h>

using namespace winrt;
using namespace Windows::Foundation;
using namespace Windows::Web::Syndication;

concurrency::task<std::wstring> RetrieveFirstTitleAsync()
{
    return concurrency::create_task([]
        {
            Uri rssFeedUri{ L"https://blogs.windows.com/feed" };
            SyndicationClient syndicationClient;
            SyndicationFeed syndicationFeed{ syndicationClient.RetrieveFeedAsync(rssFeedUri).get() };
            return std::wstring{ syndicationFeed.Items().GetAt(0).Title().Text() };
        });
}

int main()
{
    winrt::init_apartment();

    auto firstTitleOp{ RetrieveFirstTitleAsync() };
    // Do other work here.
    std::wcout << firstTitleOp.get() << std::endl;
}

參數傳遞

對於同步函式,你應該預設使用 const& 參數。 這樣可以避免複製所帶來的額外開銷(因為這會涉及參考計數,也就是互鎖的遞增與遞減操作)。

// Synchronous function.
void DoWork(Param const& value);

但是,如果你將參照參數傳遞給協程,就可能會遇到問題。

// NOT the recommended way to pass a value to a coroutine!
IASyncAction DoWorkAsync(Param const& value)
{
    // While it's ok to access value here...

    co_await DoOtherWorkAsync(); // (this is the first suspension point)...

    // ...accessing value here carries no guarantees of safety.
}

在協程中,執行在第一個暫停點之前是同步的,當控制權回到呼叫者,呼叫框架就會超出範圍。 當協程恢復時,參考參數所參考的來源值可能已經發生了任何變化。 從協程的角度來看,參考參數的壽命是無法控制的。 所以,在上述例子中,在 co_await 之前,我們都可以安全地存取 value,但在它之後就不行了。 若呼叫者破壞該 ,之後嘗試在協程內存取該值會導致記憶體損毀。 如果 DoOtherWorkAsync 有可能被暫停,然後在恢復後嘗試使用,我們也無法安全地將傳給 DoOtherWorkAsync

為了讓參數在暫停與恢復之後仍能安全使用,您的協同程式中的參數預設應採用傳值方式,以確保會以值捕捉,並避免生命週期問題。 在你確信這樣做是安全的情況下,可以偏離該項指引的情況將會非常罕見。

// Coroutine
IASyncAction DoWorkAsync(Param value); // not const&

傳遞值要求參數移動或複製成本低;而這通常是智慧指標的常態。

也有人認為,(除非你想移動該值)採用 const 值傳遞是個好做法。 這不會影響你複製的來源值,但能清楚表達意圖,且如果你不小心修改了複製品,也能有所幫助。

// coroutine with strictly unnecessary const (but arguably good practice).
IASyncAction DoWorkAsync(Param const value);

另見 標準陣列與向量,該模組探討如何將標準向量傳遞給非同步 callee。

如果你無法更改協程的簽名,但可以更改實作,那麼你可以在第一個 co_await之前先做一個本地副本。

IASyncAction DoWorkAsync(Param const& value)
{
    auto safe_value = value;
    // It's ok to access both safe_value and value here.

    co_await DoOtherWorkAsync();

    // It's ok to access only safe_value here (not value).
}

如果 Param 的複製代價很高,那麼請在第一次 co_await 之前,只擷取你需要的部分。

IASyncAction DoWorkAsync(Param const& value)
{
    auto safe_data = value.data;
    // It's ok to access safe_data, value.data, and value here.

    co_await DoOtherWorkAsync();

    // It's ok to access only safe_data here (not value.data, nor value).
}

安全存取類別成員協程中的 指標

請參見 C++/WinRT 中的強參考與弱參考

重要 API