Windows UWP 开发 - 异步编程

在 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 运行

时间: 2024-08-01 06:21:27

Windows UWP 开发 - 异步编程的相关文章

Windows UWP 开发 - 前言

Windows 10 发布近一年了,Visual Studio 2015 也已推出 Update2,UWP 应用开发不仅时机成熟了而且也已经很方便了.所以我打算写一系列的文章来记录我是如何开发 UWP 应用的,对于我自己来说算是笔记,同时也供其它朋友参考.UWP 应用和之前的 WPF.Silverlight 非常相似,其 UI 部分基于 DirectX 技术,使用 XAML 描述构建,即灵活效果也很出色.逻辑代码支持 C++.C#.VB 和 JavaScript,因此对于绝大多数程序员来说开发语

Windows UWP开发系列 – RelativePanel

RelativePanel是在Windows 10 UWP程序中引入的一种新的布局面板,它是通过附加属性设置元素间的位置关系来对实现布局的.一个简单的示例如下: <RelativePanel>????<TextBox x:Name="textBox1" Text="textbox" Margin="5"/>????<Button x:Name="blueButton" Margin="5

Windows UWP开发系列 – 3D变换

在Win8.1中,引入了一个PlaneProjection可以实现3D变换,但它的变换方式比较简单,只能实现基本的旋转操作.在Windows 10 UWP中,引入了一个更加强大的3D变换Transform3D,系统默认内置了两中变换方式:PerspectiveTransform3D和CompositeTransform3D.一个简单的示例如下: <StackPanel HorizontalAlignment="Center"> <Image Source="

Windows UWP开发系列 – 控件默认样式

今天用一个Pivot控件的时候,想修改一下它的Header样式,却发现用Blend和VS无法导出它的默认样式了,导致无法下手,不知道是不是Blend的bug. 在网上搜了一下,在MSDN上还是找到了它的默认样式的,位置如下:https://msdn.microsoft.com/en-us/library/windows/apps/mt299142.aspx.其它的控件默认样式这个地址上也有,如果有需要的可以查询一下.

Win8.1应用开发之异步编程

在win8应用商店开发时,我们会遇到许多异步方法,它们存在的目的就是为了确保你的应用在执行需要大量时间的任务时仍能保持良好的响应,也就是说调用异步API是为了响应用户的操作.设想一下我们点击一个Button,会从网上下载一些信息,如果没有异步,我们就不得不等它下载完才能继续进行操作.为了能在下载时保持响应,windows提供了一个用于下载源的异步方法SyndicationClient.RetrieveFeedAsync. // Put the keyword, async on the decl

C# 异步编程1 APM模式异步程序开发

C#已有10多年历史,单从微软2年一版的更新进度来看活力异常旺盛,C#中的异步编程也经历了多个版本的演化,从今天起着手写一个系列博文,记录一下C#中的异步编程的发展历程.广告一下:喜欢我文章的朋友,请点下面的“关注我”.谢谢 我是2004年接触并使用C#的,那时C#版本为1.1,所以我们就从就那个时候谈起.那时后在大学里自己看书写程序,所写的程序大都是同步程序,最多启动个线程........其实在C#1.1的时代已有完整的异步编程解决方案,那就是APM(异步编程模型).如果还有不了解“同步程序.

C# 异步编程2 EAP 异步程序开发

在前面一篇博文记录了C# APM异步编程的知识,今天再来分享一下EAP(基于事件的异步编程模式)异步编程的知识.后面会继续奉上TPL任务并行库的知识,喜欢的朋友请持续关注哦. EAP异步编程算是C#对APM的一种补充,让异步编程拥有了一系列状态事件.如果你看过本系列的前一篇文章<C# 异步编程1 APM 异步程序开发>,并假设你是微软C#语言开发组的一员,现在让你来设计基于事件的异步编程模式.那你是会利用之前的APM进行改造?还是进行再次创造呢?所以当你对相关dll进行反编译,会惊喜的发现EA

基于Prism.Windows的UWP开发备忘

以前做UWP开发都是使用MvvmLight,主要是简单易上手,同时也写了很多MvvmLight的开发系列文章: UWP开发必备以及常用知识点总结 UWP开发之Mvvmlight实践九:基于MVVM的项目架构分享 UWP开发之Mvvmlight实践八:为什么事件注销处理要写在OnNavigatingFrom中 UWP开发之Mvvmlight实践七:如何查找设备(Mobile模拟器.实体手机.PC)中应用的Log等文件 UWP开发之Mvvmlight实践六:MissingMetadataExcept

8天玩转并行开发——第六天 异步编程模型

在.net里面异步编程模型由来已久,相信大家也知道Begin/End异步模式和事件异步模式,在task出现以后,这些东西都可以被task包装 起来,可能有人会问,这样做有什么好处,下面一一道来. 一: Begin/End模式 1: 委托 在执行委托方法的时候,我们常常会看到一个Invoke,同时也有一对你或许不常使用的BeginInvoke,EndInvoke方法对,当然Invoke方法 是阻塞主线程,而BeginInvoke则是另开一个线程. 1 class Program 2 { 3 sta