本主题介绍 C++/WinRT 中并发和异步的高级方案。
有关此主题的简介,请先阅读 并发和异步操作。
将工作转交给 Windows 线程池
协程和其他函数一样,调用方会一直被阻塞,直到该函数将执行权返回给调用方。 协程第一次有机会返回是在第一个 co_await、co_return 或 co_yield 处。
因此,在协程中执行计算密集型工作之前,你需要先将执行权交还给调用方(换句话说,引入一个挂起点),这样调用方就不会被阻塞。 如果尚未通过 co_await-ing 其他操作执行此操作,则可以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 异常,因为必须从创建它的线程(即 UI 线程)更新 TextBlock 。 一种解决方案是捕获最初调用我们的协程时所在的线程上下文。 为此,请实例化一个 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;。
协程中的执行上下文、恢复与切换
广义上说,在协程中的挂起点之后,原先的执行线程可能不再存在,而恢复执行则可能发生在任何线程上(换句话说,任何线程都可能调用该异步操作的 Completed 方法)。
但是,如果你 co_await 四种 Windows 运行时 异步操作类型中的任意一种(IAsyncXxx),那么 C++/WinRT 会在你 co_await 时捕获调用上下文。 它还能确保在继续执行恢复时,你仍处于该上下文中。 C++/WinRT 的做法是先检查你是否已经处于调用上下文中;如果没有,就切换到该上下文。 如果你在 co_await 之前位于单线程单元(STA)线程上,那么之后你仍会在同一个线程上;如果你在 co_await 之前位于多线程单元(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 在异步操作完成时所在的同一线程上恢复执行(这样就不会发生上下文切换),那么可以先编写类似于下面所示的 await 适配器。
注释
下面的代码示例仅用于教育目的;它让你开始了解 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++ 编译器不再查找与 IAsyncXxx 匹配的三个 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;
...
}
注释
如上所示,请务必包含你正在 co_await 的类型所属命名空间的投影头文件。 例如,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() 进度令牌上使用该方法。
注释
报告临时结果需要 C++/WinRT 版本 2.0.210309.3 或更高版本。
上面的示例选择为每个进度报告设置临时结果。 可以选择随时报告临时结果(如果有)。 它不需要与进度报告结合。
注释
为异步动作或操作实现一个以上的 完成处理程序 是不正确的。 可以为其完成事件指定单个委托,也可以对其使用 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 期间,控制权已返回给调用方,调用帧已超出作用域,因此当协程恢复执行时,你将无法确定该句柄是否仍然有效。
严格来说,我们的协程正如预期的那样,以值传递方式接收参数(参见上文参数传递)。 但在这种情况下,我们需要更进一步,以确保我们遵循的是该指导意见的精神(而不仅仅是字面意思)。 我们需要连同句柄一起传递强引用(换句话说,即所有权)。 操作方法如下。
IAsyncAction Async(winrt::handle event)
{
co_await DoWorkAsync();
co_await resume_on_signal(event); // The incoming handle *is* valid here.
}
将 winrt::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");
}
}
注释
wait_for 在接口中使用 std::chrono::duration,但其可用范围受限,小于 std::chrono::duration 所提供的范围(大约为 49.7 天)。
下一个示例中 的wait_for 等待大约 5 秒,然后检查完成情况。 如果比较结果符合预期,那么你就知道该异步对象已成功完成,这样就可以了。 如果等待某个结果,只需调用 GetResults 方法来检索结果即可。
注释
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 方法检索任何结果之前轮询其状态。 但这时最好直接丢弃该对象。
另一种做法是仅检查 Started,并让 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:[msg]语法错误 [context]:预期>或接近“[”。
Windows.Foundation.IAsyncOperation<Int32[]> RetrieveArrayAsync();
原因是将数组用作参数化接口的参数类型参数无效。 因此,我们需要一种不太明显的方法来实现从运行时类方法异步传递数组的目标。
可以返回封装在 PropertyValue 对象中的数组。 然后,调用方代码会将其拆箱。 下面是一个代码示例,你可以尝试将 SampleComponent 运行时类添加到 Windows 运行时 Component (C++/WinRT) 项目中,然后在(例如)Blank App, Packaged (WinUI 3 in Desktop) 项目中使用它。
// 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 接口
- IAsyncActionWithProgress TProgress<> 接口
- IAsyncOperation<TResult> 接口
- IAsyncOperationWithProgress<TResult, TProgress> 接口
- SyndicationClient::RetrieveFeedAsync 方法
- winrt::fire_and_forget
- winrt::get_cancellation_token
- winrt::get_progress_token
- winrt::resume_foreground