[Windows翻译]在C++/WinRT中创建非敏捷委托,第4部分:从后台线程同步等待。

182 阅读3分钟

原文地址:devblogs.microsoft.com/oldnewthing…

原文作者:devblogs.microsoft.com/oldnewthing…

发布时间:2020年4月9日

警告那些偶然发现这个页面的人。不要在没有读完的情况下使用本页的代码.

本周,我们组装了一个函数 resume_synchronous,它可以同步恢复在另一个公寓中的执行。这个用例是一个在后台线程上运行的delegate,需要在UI线程上同步运行代码。

你可以使用C++/WinRT内置的东西来获得同样的效果:你可以在Windows Runtime异步操作上调用get()方法,C++/WinRT会阻塞调用公寓,直到异步操作完成。

winrt::IAsyncAction DoActualWorkAsync(
    CoreDispatcher dispatcher, DeviceInformation info)
{
  co_await winrt::resume_foreground(dispatcher);
  viewModel.Append(winrt::make<DeviceItem>(info));
}

deviceWatcher.Added(
    [=](auto&& sender, auto&& info)
    {
        DoActualWorkAsync(Dispatcher(), info).get();
    });

这里的想法是我们从调用DoActualWorkAsync开始,DoActualWorkAsync异步地完成它的工作。但是,我们不是协同等待结果,而是得到()它。get()方法是同步地等待操作完成。

只建议从后台线程同步等待异步操作完成。从UI线程执行同步等待,自然会使你的程序在等待期间无响应。但比这更糟糕的是。异步操作可能本身也想使用UI线程,但由于你阻止了它,它就不能使用了。结果就是你的UI线程出现了死锁,这让大家都很伤心。

现在,把异步部分拆成一个单独的函数是有点麻烦的,因为你想在UI线程上使用的所有东西都需要作为参数传递进来。也许我们可以做得更好。

template<typename TLambda>
winrt::IAsyncAction DispatchAsync(
    CoreDispatcher dispatcher, TLambda&& lambda)
{
  co_await winrt::resume_foreground(dispatcher);
  lambda();
}

deviceWatcher.Added(
    [=](auto&& sender, auto&& info)
    {
        DispatchAsync(Dispatcher(), [&]
        {
            viewModel.Append(winrt::make<DeviceItem>(info));
        }).get();
    });

这个版本接受一个lambda,并在切换到dispatcher的UI线程后运行它。

但是这段代码看起来完全不对。我们通过引用来获取对象,并在一个悬空的co_await边界上使用这些对象!这不是在给我们带来麻烦吗?在引用者可能已经被破坏之后再使用引用,这难道不是灾难的根源吗?

是的,这在一般情况下是危险的,但在这个特定的情况下是可行的,因为外部的 IAsyncAction 不是 co_await,而是传递给 get() ,后者执行同步等待。因此,在coroutine的生命周期内,所有的参数都将保持有效,因为参数的销毁要到get()中结束的 "完整表达式 "结束时才会发生。

一旦我们意识到这一点,我们就可以更进一步。

deviceWatcher.Added(
    [=](auto&& sender, auto&& info)
    {
        [&]() -> winrt::IAsyncAction
        {
            co_await winrt::resume_foreground(Dispatcher());
            viewModel.Append(winrt::make<DeviceItem>(info));
        }().get();
    });

lambda不会被销毁,直到完整表达式结束,这发生在get()返回之后,这意味着所有的引用捕获将在lambda的生命周期内保持有效。

我想你可以把这个因素考虑进去。

template<typename TLambda>
void RunSyncOnDispatcher(
    CoreDispatcher const& dispatcher,
    TLambda&& lambda)
{
  [&]() -> winrt::IAsyncAction
  {
    co_await winrt::resume_foreground(dispatcher);
    lambda();
  }().get();
}

deviceWatcher.Added(
    [=](auto&& sender, auto&& info)
    {
        RunSyncOnDispatcher(Dispatcher(), [&]()
        {
            viewModel.Append(winrt::make<DeviceItem>(info));
        });
    });

这确实造成了风险,有人会传递一个本身就是coroutine的lambda。由于RunSyncOnDispatcher并不co_await lambda的结果,所以同步执行只持续到lambda到达它的第一个暂停co_await为止,这使得lambda的其他coroutine在没有get()的保护下执行。

然而,这段代码中有一个偷偷的bug。我们下次再看。


www.deepl.com 翻译