原文地址: 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。