這很重要
本主題介紹 協同程式 與 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 執行階段 命名空間包含四種非同步操作物件類型。
- IAsyncAction,
- IAsyncActionWithProgress<TProgress>,
- IAsyncOperation<TResult>,以及
- IAsyncOperationWithProgress<TResult, TProgress>。
這些非同步操作類型會投影到對應的 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 執行階段 函式或從中傳回的類型也算符合條件(例如 int 或 winrt::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
- 並行::任務類別
- IAsyncAction 介面
- IAsyncActionWithProgressTProgress<> 介面
- IAsyncOperation<TResult> 介面
- IAsyncOperationWithProgress<TResult, TProgress> 介面
- SyndicationClient::RetrieveFeedAsync 方法
- SyndicationFeed 類別