[Windows翻译]取消一个Windows Runtime异步操作,第2部分:C++/CX与PPL,显式延续风格。

170 阅读2分钟

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

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

发布时间:2020年7月2日

我们从C#中如何投射任务取消开始研究Windows Runtime取消。但是,C++/CX中的PPL和显式连续如何呢?

好吧,让我们这样做。

auto picker = ref new FileOpenPicker();
picker->FileTypeFilter.Append(L".txt");

cancellation_token_source cts;
auto do_cancel = std::make_shared<call<bool>>([cts](bool) { cts.cancel(); });
auto delayed_cancel = std::make_shared<timer<bool>>(3000U, false, do_cancel.get());
delayed_cancel->start();

create_task(picker->PickSingleFileAsync()).
    then([do_cancel, delayed_cancel](task<StorageFile^> precedingTask)
    {
        StorageFile^ file;
        try {
            file = precedingTask.get();
        } catch (task_canceled const&) {
            file = nullptr;
        }

        if (file != nullptr) {
            DoSomething(file);
        }
    });

设置定时器来取消任务是相当麻烦的。调用对象和定时器对象都是不可复制的,但我们需要在异步操作期间保持这两个对象的活力,所以我们需要将它们复制到lambda中,这样它们就不会过早地被销毁。但是你就会遇到 "不可复制 "的问题。

你的下一个想法是直接将对象初始化到lambda中。

    then([do_cancel = call<bool>(...),
          delayed_cancel = timer<bool>(...)]
         (task<StorageFile^> precedingTask)

但是这也不行,因为lambda会被PPL内部复制,所以我们又一次遇到了 "不可复制 "的问题。

我们通过把调用和定时器都放在一个shared_ptr中来解决这个问题。这个shared_ptr是可以复制的,当最后一个destructs时,call和timer就会被销毁。

好了,说了一大堆烦人的事。

当底层的Windows Runtime异步操作完成时,PPL会将状态传播到任务中。你可以在ppltasks.h中看到这种情况的发生(为了便于说明,我把代码简化了一些)。

_AsyncOp->Completed = ref new AsyncOperationCompletedHandler<_ReturnType>(
              [_OuterTask](auto^ _Operation, AsyncStatus _Status) mutable
{
    if (_Status == AsyncStatus::Canceled)
    {
        _OuterTask->_Cancel(true);
    }
    else if (_Status == AsyncStatus::Error)
    {
        _OuterTask->_CancelWithException(
            std::make_exception_ptr(::ReCreateException(_Operation->ErrorCode.Value)));
    }
    else
    {
        _ASSERTE(_Status == AsyncStatus::Completed);

         try
        {
            _OuterTask->_FinalizeAndRunContinuations(_Operation->GetResults());
        }
        catch (...)
        {
            // unknown exceptions thrown from GetResult
            _OuterTask->_CancelWithException(std::current_exception());
        }
}

当操作完成后,PPL会查看状态码。如果状态码说操作被取消了,那么它就取消包装任务。如果说操作遇到了错误,那么它就会根据错误代码合成一个异常对象,并把它放在包装任务中。否则,操作成功了,所以我们从操作中得到结果(_Operation->GetResults()),并将其设置为包装任务的结果。(还有一个额外的问题:如果GetResults本身抛出了一个异常,那么包装任务就会被设置为带有该异常的错误状态。)

好了,这就是取消任务是如何进入包装任务的。它是怎么出来的呢?

当你试图获取一个取消任务的结果时,PPL会抛出一个task_canceled对象。这在task.get()下有记载,你可以在ppltask.h中看到它的发生。

_ReturnType get() const
{
    if (!_M_Impl)
    {
        details::_DefaultTaskHelper::_NoCallOnDefaultTask_ErrorImpl();
    }

    if (_M_Impl->_Wait() == canceled)
    {
        _THROW(task_canceled{});
    }

    return _M_Impl->_GetResult();
}

下一次,我们将研究PPL与coroutines。