异步与Windows应用程序

把async关键字用于UWP应用程序与本章前面的相同。但需要注意,在UI线程中调用await之后,当异步方法返回时,将默认返回到UI线程中。这便于在异步方法完成后更新UI元素。

注:为了创建UWP应用程序,需要Windows 10,Windows系统必须在"开发人员模式"下配置。启用"开发人员模式"时,需要打开Windows设置,选择Update & Security磁贴,选择For developers类别,并单击单选按钮Developer mode。这样系统就可以运行旁路的应用程序了(未从Windows Store中安装的应用程序),并为"开发人员模式"添加一个Windows包。

为了理解功能和问题,创建一个通用Windows应用程序。这个应用程序包含5个按钮和一个TextBlock元素,来演示不同的场景:

       <StackPanel>
            <Button Content="Start Async" Click="OnStartAsync" Margin="4"/>
            <Button Content="Start Async with Config" Click="OnStartAsyncConfigureAwait" Margin="4"/>
            <Button Content="Start Async with Thread Switch" Click="OnStartAsyncWithThreadSwitch" Margin="4"/>
            <Button Content="Use IAsyncOperation" Click="OnIAsyncOperation" Margin="4"/>
            <Button Content="Deadlock" Click="OnStartDeadlock" Margin="4"/>
            <TextBlock x:Name="text1" Margin="4"/>
        </StackPanel>

在OnStartAsync方法中,UI线程的线程ID写入TextBlock元素。接下来调用异步方法Task.Delay,它不阻塞UI线程,在此方法完成后,线程ID将再次写入TextBlock:

        private async void OnStartAsync(object sender, RoutedEventArgs e)
        {
            text1.Text = $"UI thread: {GetThread()}";
            await Task.Delay(1000);
            text1.Text += $"\n after await: {GetThread()}";
        }

为了访问线程ID,可以使用Environment类。在UWP应用程序中,Thread类是无效的——至少在构建版本15063之前是这样的:

private string GetThread() => $"thread: {Environment.CurrentManagedThreadId}";

验证了下,在当前构建版本下,Thread类可用。

private string GetThread() => $"thread: {Thread.CurrentThread.ManagedThreadId}";

运行应用程序时,可以在文本元素中看到类似的输出。与控制台应用程序相反,UWP应用程序定义了一个同步上下文,在等待之后,可以看到与以前相同的线程。这允许直接访问UI元素:

UI thread: thread: 3
after await: thread: 3

配置await

如果不需要访问UI元素,就可以配置await,以避免使用同步上下文。下面的代码片段演示了配置,并说明为什么不应该从后台线程上访问UI元素。

使用OnStartAsyncConfigureAwait方法,在将UI线程的ID写入文本输入后,将调用本地函数AsyncFunction。在这个本地函数中,启动线程是在调用异步方法Task.Delay之前写入的。使用此方法返回的任务,将调用ConfigureAwait。在这个方法中,任务的配置是通过传递设置为false的continueOnCapturedContext参数来完成的。通过这种上下文配置,会发现等待之后的线程不再是UI线程。使用不同的线程将结果写入result变量即可。如try块所示,千万不要从非UI线程中访问UI元素。得到的异常包含HREUSLT值,如when子句所示。只有这个异常在catch中捕获:结果返回给调用者。对于调用方,也调用了ConfigureAwait,但是这次,continueOnCapturedContext设置为ture。在这里,在等待之前和之后,方法都在UI线程中运行:

        private async void OnStartAsyncConfigureAwait(object sender, RoutedEventArgs e)
        {
            text1.Text = $"UI thread: {GetThread()}";
            string s = await AsyncFunciton().ConfigureAwait(continueOnCapturedContext: true);
            //after await,with continueOnCapturedContext true we are back in the UI thread
            text1.Text += $"{s}\nafter await: {GetThread()}";
            async Task<string> AsyncFunciton()
            {
                string result = $"\nasync function: {GetThread()}";
                await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false);
                result += $"\nasync function after await: {GetThread()}";
                try
                {
                    text1.Text = "this is a call from the wrong thread";
                    return "not reached";
                }
                catch (Exception ex) when (ex.HResult == -2147417842)
                {
                    return result;
                    //we know it's the wrong thread
                    //don't access UI elements from the previous try block
                }
            }
        }

运行应用程序时,可以看到如下输出。在等待后的异步本地函数中,使用了另一个线程。文本not reached从来没有写过,因为抛出了异常:

UI thread: thread: 3
async function: thread: 3
async function after await: thread: 4
after await: thread: 3

警告:在后面的UWP内容中,使用了数据绑定,而不是直接访问UI元素的属性。但是,在UWP中,也不能在非UI线程中编写绑定到UI元素的属性。

切换到UI线程

在某些情况下,使用后台线程访问UI元素并不容易。在这里,可以使用从Dispacther属性返回的CoreDispatcher对象切换到UI线程。Dispatcher属性在DependencyObject类中定义。DependencyObject是UI元素的基类。调用CoreDispatcher对象的RunAsync方法会在UI线程中再次运行传递进来的lambda表达式:

        private async void OnStartAsyncWithThreadSwitch(object sender, RoutedEventArgs e)
        {
            text1.Text = $"UI thread: {GetThread()}";
            string s = await AsyncFunciton();
            //after await,with continueOnCapturedContext true we are back in the UI thread
            text1.Text += $"{s}\nafter await: {GetThread()}";
            async Task<string> AsyncFunciton()
            {
                string result = $"\nasync function: {GetThread()}";
                await Task.Delay(1000).ConfigureAwait(continueOnCapturedContext: false);
                result += $"\nasync function after await: {GetThread()}";
                await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
                 {
                     text1.Text += $"\nasync function switch back to the UI thread: {GetThread()}";
                 });
                return result;
            }
        }

运行应用程序时,可以看到在使用RunAsync时总是使用的UI线程。

UI thread: thread: 3
async function switch back to the UI thread: thread: 3
async function: thread: 3
async function after await: thread: 5
after await: thread: 3

使用IAsyncOperation

异步方法由Windows运行库定义,不返回Task或ValueTask。Task和ValueTask不是Windows运行库的一部分。相反,这些方法返回一个实现接口IAsyncOperation的对象,IAsyncOperation并没有根据需要通过await关键字来定义方法GetAwaiter。但是使用await关键字时,IAsyncOperation会自动转换为Task。还可以使用AsTask扩展方法将IAsyncOperation对象转换为任务。

在示例应用程序的方法OnIAsyncOperation中,调用MessageDialog的ShowAsync()方法。该方法返回一个IAsyncOperation,可以简单地使用await关键字获取结果:

        private async void OnIAsyncOperation(object sender, RoutedEventArgs e)
        {
            var dlg = new MessageDialog("Select One,Two,Or Three", "Sample");
            dlg.Commands.Add(new UICommand("One",null,1));
            dlg.Commands.Add(new UICommand("Two", null, 2));
            dlg.Commands.Add(new UICommand("Three", null, 3));
            IUICommand command = await dlg.ShowAsync();
            text1.Text = $"Command {command.Id} with the label {command.Label} invoked";
        }

避免阻塞情况

在Task上一起使用Wait和async关键字是很危险的。在使用同步上下文的应用程序中,这很容易导致死锁。

在方法OnStartDeadlock中,调用本地函数DelayAsync。DelayAsync等待Task.Delay的完成,之后在前台线程中继续执行。但是,调用者在DelayAsync返回的任务上调用Wait()方法。Wait()方法阻塞调用线程。直到任务完成。在这种情况下,Wait()是从前台线程上调用的,因此Wait()会阻塞前台线程。Task.Dealy上的Wait()永远无法完成,因为前台线程不可用。这是一个经典的死锁场景:

        private void OnStartDeadlock(object sender, RoutedEventArgs e)
        {
            DelayAsync().Wait();
            async Task DelayAsync()
            {
                await Task.Delay(1000);
            }
        }

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值