自从.NET 4.5开始,C#多了一对新的异步关键词 async 和 await,如果不了解的朋友可以简单的看下下面的示意图。
简单的说,就是在通常情况下,用户在界面上进行的操作,比如点击一个按钮之后,如果进行大量的计算,或者读写文件、网络请求等耗时的操作,那么程序的界面就会卡住,在这段时间里,任何交互都不会响应,直到后台的代码执行完毕才会继续响应用户操作。
这种现象和你的应用是不是UWP没有什么卵关系,WinForm,WPF,Windows Phone SL/RT应用都会有这个问题。
这个问题的原因是因为这些耗时的操作和界面(UI线程)没有关系,但却在UI线程上执行了。解决办法就是另起一个线程,让它在UI线程之外去执行代码,这样就不会锁死UI。就像这图:
在.NET 4.5以前,要进行一个异步操作写法有点繁琐,.NET 4.5以后。我们只要把一个方法的签名声明为async Task,就可以直接变成异步方法。然后被调用者await。在UI线程上await后台操作,是不会卡住界面的。当然,前提是这个方法体内本身有异步API。不清楚的朋友可以去MSDN看看async await的介绍:
使用 Async 和 Await 的异步编程(C# 和 Visual Basic)
好了,话题回到UWP应用,今天我无聊的时候写了个算24点解法的应用:
问题是后台计算24点的解法需要几秒钟时间,用户点击“求解”按钮以后,界面就卡住,直到计算完毕。
原先的代码如下:
... public ObservableCollection<string> ResultList { get { return _resultList; } set { _resultList = value; RaisePropertyChanged(); } } private void DoCalc() { ResultList.Clear(); // ... 各种计算逻辑 foreach (一个神器的循环) { ResultList.Add($"{expression} = {value}"); } } ...
这里的蛋疼之处在于,DoCalc()方法里没有任何.NET已提供的async API,没有await的机会。如果是.NET自带的异步API,比如写文件和网络请求(.NET Http Client),就可以天生被await。
.NET里处理这种情况,用的是Task.Run(),它可以接受一个委托,然后起一个新线程去执行这个委托。由于返回类型是Task,所以可以被await。
public static Task Run(Action action);
所以DoCalc的方法签名就可以改成异步的了:
private async Task DoCalc()
然后里面塞个Task.Run(),然后把计算逻辑包在委托里面
private void DoCalc() { ResultList.Clear(); await Task.Run(()=> { // ... 各种计算逻辑 foreach (一个神器的循环) { ResultList.Add($"{expression} = {value}"); } }); }
但是这样是会爆炸的!原因是ResultList还在主线程(UI线程)上,Task新起的线程会跨线程访问这个ResultList变量,就会爆炸。
所以我们的思路是让这个计算逻辑带返回值,算完以后把结果扔回UI线程,再给ResultList赋值,而不是直接更改ResultList。
所以我们要把Action委托改成Func委托:
Func<List<string>> funcCalc = () => { var tempList = new List<string>(); ... foreach (...) { tempList.Add($"{expression} = {value}"); } return tempList; };
然后这时候用Task.Run()的一个重载方法去执行,就能得到返回值:
public static Task<TResult> Run<TResult>(Func<TResult> function);
然后再给UI线程上的ResultList赋值:
var list = await Task.Run(funcCalc); ResultList = list.ToObservableCollection();
为了让交互更加友好,可以在界面里加个Progress Ring显示忙操作的状态:
<ProgressRing Height="50" Width="50" IsActive="{Binding IsBusy}" />
最后完工的异步方法如下:
private async Task DoCalc() { IsBusy = true; ResultList.Clear(); Func<List<string>> funcCalc = () => { var tempList = new List<string>(); ... foreach (...) { tempList.Add($"{expression} = {value}"); } return tempList; }; var list = await Task.Run(funcCalc); ResultList = list.ToObservableCollection(); IsBusy = false; }
最后要说的是,这种方法只针对由于大量CPU计算、磁盘IO和网络请求等非界面的逻辑造成的卡死,如果你的XAML写的像翔一样的,那么很有可能界面卡死是因为数据绑定、长列表没做虚拟化等等,那就另当别论啦~