标签:
在 Windows UWP 开发中最基础也是最重要的就是异步编程,Windows Runtime 库,也就是 RT 库,其中的很多函数都是 async 结尾的,比如 PickSingleFolderAsync,凡是此类函数都是异步操作。
MSDN 上的异步编程指南:https://msdn.microsoft.com/zh-cn/library/windows/apps/mt187340.aspx
在 Windows UWP C++/CX 中进行异步编程是非常容易的,有着基本固定的格式,如下。
create_task([]() { //...... }).then([]() { //...... }).then([]() { //...... });
微软称这种模式为“任务链”。也就是用 then 把一些 lambda 函数串联起来。大多数情况下这种模式都是可行的,但极个别情况下可能会用到另一种形式。
create_task([]() { create_task([]() { //...... }).get(); }).then([]() { create_task([]() { //...... }).then([]() { //...... }); });
这种嵌套的方式也是可行的,但通常会让代码变得非常复杂,特别是在需要异常处理时更是如此。
异步编程的形式并不复杂,但有 3 项内容需要注意。
1. UI 线程与 Async 异步线程
在基于 XAML 的 UWP 应用中每一个窗口只有一个 UI 线程,所有与 UI 相关的取值赋值等操作都必须在 UI 线程上进行。而 task 任务是在一个异步线程中进行的,在异步线程中是不能与 UI 部分互动的。但是在完成逻辑运算后通常要将结果反馈给用户,因此必须返回到 UI 线程中去。同样如果在返回到 UI 线程之后还有进一步的逻辑运算,则需要再次进入到异步线程中。完成这样的线程交叉转换是非常容易的,仅仅只要在 then 部分的延续任务中传入一个参数即可,如下。
create_task([]() { //Async }).then([]() { //UI }, task_continuation_context::use_current()).then([]() { //Async }, task_continuation_context::use_arbitrary()).then([]() { //UI }, task_continuation_context::use_current());
use_current() 这个 static 函数返回的 task_continuation_context 表示这部分延续将在 UI 线程上进行,而 use_arbitrary 则意味着任务将在异步线程中执行。如果在调试时收到“HRESULT:0x8001010E 应用程序调用一个已为另一线程整理的接口”这一错误,就说明在异步线程中出现了与 UI 互动的代码,找到并将这些代码放入 UI 线程即可。传参数这种方式是首选方式,但有时还需要另一种解决方案,也就是使用 CoreDispatcher。
create_task([this]() { Dispatcher->RunAsync(CoreDispatcherPriority::Normal, ref new DispatchedHandler([]() { //...... })); });
这种方式最常见的就是用在一个循环运算中不断更新 UI 界面上的 Progress 控件值。所有从 DependencyObjec 派生的类中都有 Dispatcher 属性,一般直接从 UI 控件中取用,除此之外还可以从 Window::Current 或 CoreWindow::GetForCurrentThread() 中取用。不过要是想使用 Window::Current 或 CoreWindow::GetForCurrentThread() 的 Dispatcher,必须从 UI 线程上访问,换句话说 Current Thread 必须是当前的 UI 线程。例如:
create_task([dispatcher = Window::Current->Dispatcher]() { dispatcher->RunAsync(CoreDispatcherPriority::Normal, ref new DispatchedHandler([]() { //...... })); });
还有一点就是 task_continuation_context 和 CoreDispatcher 都可以作为参数传递。比如:
create_task([tcc = task_continuation_context::use_current()]() { create_task([]() { //...... }).then([]() { //...... }, tcc); });
在异步线程中直接调用 task_continuation_context::use_current() 是无效的,但可以在 UI 线程中取得,然后作为参数传入。
2. 异常处理
异常处理是通过 task 实例中的 get 函数来抛出的,如下。
create_task([]() { throw 0; }).then([]() { throw 1; }).then([](task<void> t) { try { t.get(); } catch (const int& e) { switch (e) { case 0: OutputDebugStringW(L"0\n"); break; default: OutputDebugStringW(L"1\n"); break; } } });
运行这样一段代码会得到结果“0”,而不是结果“1”。而运行下面这段代码就能得到“1”了。
create_task([]() { throw 0; }).then([](task<void> t) { try { t.get(); } catch (...) {} throw 1; }).then([](task<void> t) { try { t.get(); } catch (const int& e) { switch (e) { case 0: OutputDebugStringW(L"0\n"); break; default: OutputDebugStringW(L"1\n"); break; } } });
由此可见每调用一次 get() 就会把之前出现的异常抛出来,如果在发生异常之后没有 get() 程序就会崩溃,因此在开发 UWP 应用时务必在任务链的最后一部分中加入 get。当然是否忽略或处理异常这取决于异常的类型,不过在 UWP 中很多的异常是可以忽略或处理的。比如异步操作取消,打开文件失败等异常都是在预料之中的,是正常运行的一部分,这些异常必须要去处理,不应该由此而造成程序崩溃。关于异常还有一种特殊情况需要考虑。
task<void> DoSomethingAsync() { throw 0; return create_task([]() { //...... }); } DoSomethingAsync().then([](task<void> t) { try { t.get(); } catch (...) {} });
在这种情况下异常不是在 get() 这里抛出的。因为这段代码和下面这段其实是一样的。
throw 0; create_task([]() { //...... }).then([](task<void> t) { try { t.get(); } catch (...) {} });
所以对于这种情况,如果需要捕获异常,则必须使用如下方式。
try { DoSomethingAsync().then([](task<void> t) { try { t.get(); } catch (...) {} }); } catch (...) {}
一个 Async 结尾的函数中并不一定全部都是在异步线程中进行的,实际上在 RT 库中很多 Async 函数在开始异步之前都包含有一些 UI 线程上的操作,而有时这些操作的确会抛出异常。这些异常因为发生在异步操作之前,所以通过 get() 是捕获不到的,必须在异步代码块的外面捕获。
3. 取消异步操作
之所以使用异步操作就是因为这些操作通常会需要一段时间或者很长一段时间来完成,为了不导致 UI 界面无响应所以将其转移到异步线程中去执行,当然有时是为了并发。对于这些耗时操作很多时候都需要取消以便提前终止,因此取消是异步操作中非常重要的一项功能。在 MSDN 中微软详细介绍了异步操作取消,https://msdn.microsoft.com/zh-cn/library/windows/apps/dd984117.aspx,只不过这篇文章中的部分代码所采用的 is_task_cancellation_requested() 方法已经无效了(https://msdn.microsoft.com/en-us/library/hh750070.aspx)。微软对此也做了解释:https://connect.microsoft.com/VisualStudio/feedback/details/1032968/is-task-cancellation-requested-function-declaration-is-missing-in-ppltasks-h。对 task 的可行取消方式如下。
cancellation_token_source cts; create_task([token = cts.get_token()]() { if (token.is_canceled()) cancel_current_task(); }).then([](task<void> t) { try { t.get(); } catch (...) {} }); cts.cancel();
从一个 cancellation_token_source 实例中获取 cancellation_token,然后把它传入到异步过程中去。根据需要时不时的检测 is_canceled() 函数的返回值,如果此值为 true,则通过调用 cancel_current_task() 这个 static 函数来取消异步操作。cancel_current_task 这个函数是个 inline 函数,也就是内联函数,唯一的一句就是 throw 一个 task_canceled 实例。换句话说就是通过 throw 一个特定的异常来终止异步代码的执行,直接跳转到 get() 那里去。至于对 is_canceled 的检查频度这个没有什么标准,频繁检查当然会影响性能,但过少的检查又不能及时响应取消操作。所以根据不同的实际需求,需要斟酌的设置检查频度。
对于 task 的取消并不复杂,但 RT 库中的 Async 函数并不是 task 的,而是 IAsyncInfo 的派生类,比如 IAsyncAction,这个使得取消操作复杂化了。创建一个可取消的 Async 函数的正确方法如下。
IAsyncAction^ DoSomethingAsync() { return create_async([](cancellation_token token) { return create_task([token]() { for (auto i = 0u; i != 30u; ++i) { wait(100u); if (token.is_canceled()) cancel_current_task(); } }); }); }
对它的调用方法如下。
create_task(DoSomethingAsync(), token);
如果是在 then 中调用,则如下。
auto token = cts.get_token(); create_task([token]() { //...... if (token.is_canceled()) cancel_current_task(); //...... }).then([token]() { //...... if (token.is_canceled()) cancel_current_task(); //...... return DoSomethingAsync(); }, token, task_continuation_context::use_arbitrary()).then([](task<void> t) { try { t.get(); } catch (const task_canceled&) { //...... } catch (...) {} }, task_continuation_context::use_current());
当然并不是所有的 Async 函数都可以从异步线程中调用,因此传递 use_arbitrary 还是 use_current 取决于实际情况。另外需要注意的是在 then 中是 return DoSmethingAsync() 而不仅仅是 DoSmethingAsync()。
以下是一个完整的示例。
(1) 创建一个空白项目 AppTest
(2) 编辑 MainPage.xaml 为:
<Page x:Class="AppTest.MainPage" xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> <StackPanel> <TextBlock x:Name="TxtResult"/> <Button x:Name="BtnStart" Content="Start" Click="Button_Start"/> <Button x:Name="BtnCancel" Content="Cancel" IsEnabled="False" Click="Button_Cancel"/> </StackPanel> </Page>
(3) 编辑 MainPage.xaml.h 为:
#pragma once #include "MainPage.g.h" namespace AppTest { public ref class MainPage sealed { public: MainPage(); private: std::unique_ptr<Concurrency::cancellation_token_source> m_cts; void Button_Start(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); void Button_Cancel(Platform::Object^ sender, Windows::UI::Xaml::RoutedEventArgs^ e); }; }
(4) 编辑 MainPage.xaml.cpp 为:
#include "pch.h" #include "MainPage.xaml.h" #include <ppl.h> #include <ppltasks.h> using namespace AppTest; using namespace Concurrency; using namespace Platform; using namespace Windows::Foundation; using namespace Windows::UI::Core; using namespace Windows::UI::Xaml; using std::make_unique; MainPage::MainPage() : m_cts(nullptr) { InitializeComponent(); } IAsyncAction^ DoSomethingAsync() { return create_async([](cancellation_token token) { return create_task([token]() { for (auto i = 0u; i != 30u; ++i) { wait(100u); if (token.is_canceled()) cancel_current_task(); } }); }); } void MainPage::Button_Start(Object^ sender, RoutedEventArgs^ e) { TxtResult->Text = L"Start"; BtnCancel->IsEnabled = true; BtnStart->IsEnabled = false; m_cts = make_unique<cancellation_token_source>(); auto token = m_cts->get_token(); create_task([token]() { //...... if (token.is_canceled()) cancel_current_task(); //...... }).then([]() { return DoSomethingAsync(); }, token).then([this](task<void> t) { try { t.get(); TxtResult->Text = L"Completed"; } catch (const task_canceled&) { TxtResult->Text = L"Canceled"; } catch (...) {} BtnStart->IsEnabled = true; BtnCancel->IsEnabled = false; }, task_continuation_context::use_current()); } void MainPage::Button_Cancel(Object^ sender, RoutedEventArgs^ e) { if (m_cts != nullptr) { m_cts->cancel(); m_cts = nullptr; } BtnCancel->IsEnabled = false; }
(5) F5 运行
标签:
原文地址:http://www.cnblogs.com/x-shi/p/5488052.html