本主題描述了在 C++/WinRT 中具備並行與非同步的進階情境。
想了解這個主題,請先閱讀《 並行與非同步操作》。
將工作交由 Windows 執行緒集區處理
協程是一個與其他函式相同的函式,因為呼叫者會被阻擋,直到函式將執行回傳給它。 協程回歸的第一個機會是第一個 co_await、 co_return或 co_yield。
所以,在協程中執行計算限制工作之前,你需要先將執行回傳給呼叫者(換句話說,引入一個暫停點),以確保呼叫者不會被阻塞。 如果你還沒用其他操作來做 co_await-,那你可以 co_await 用 winrt::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_any、complete_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_ready、await_suspend 和 await_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++ 編譯器不再尋找三個符合 IAsyncXX 的 await_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 應用程式開發中 PostMessage 與 SendMessage 的差異。 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::DispatcherQueue 或 Microsoft::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 一無所知的協程或並行函式庫互通。
報告進度
如果你的協程回傳 IAsyncActionWithProgress 或 IAsyncOperationWithProgress 其中之一,則你可以取得由 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_for 和 get 是互斥的(你不能同時稱它們為兩者)。 它們各自算作一個等待器,而 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
- IAsyncAction 介面
- IAsyncActionWithProgressTProgress<> 介面
- IAsyncOperation<TResult> 介面
- IAsyncOperationWithProgress<TResult, TProgress> 介面
- SyndicationClient::RetrieveFeedAsync 方法
- Winrt::fire_and_forget
- Winrt::get_cancellation_token
- Winrt::get_progress_token
- winrt::resume_foreground