Asynchronous in .Net 4.5
当我们在界面线程中进行I/O操作的时候,经常会导致界面锁死。为了解决这个问题,就需要将I/O操作异步执行。
在.Net 4.5中针对异步编程有了新的做法,它新推出了两个关键字 async 和 await。
下面我们就以读取网络数据为例,一步一步来看看它能带给我们的优势。
1. 一般的同步实现方式
public string DownloadSync() { StringBuilder sb = new StringBuilder(); using (WebClient client = new WebClient()) { for (int i = 1; i <= 10; ++i) { string url = string.Format(urlTemplate, i); string html = client.DownloadString(url); sb.Append(html); } } return sb.ToString(); }
调用:
string content = DownloadSync(); MessageBox.Show(string.Format("下载完成!共{0}字节", content.Length));
我们通过for循环来获取KDS论坛前10页的HTML网络数据。这种实现方式不适合在界面中执行,在数据没有获取完之前,界面会一直处于假死状态。
2. .Net 4.5之前的异步实现方式
比较偷懒的方式解决界面假死问题,我们可以new Thread(…).Start();来执行以上同步的代码。但是WebClient支持异步执行。
首先我们设计一个方法:
public void DownloadAsync(StringBuilder sb, int startIndex, int count, Action finishBlock) { WebClient client = new WebClient(); client.DownloadStringCompleted += (o, e) => { sb.Append(e.Result); if (startIndex == count) { if (finishBlock != null) finishBlock(); } else { DownloadAsync(sb, startIndex + 1, 10, finishBlock); } }; string url = string.Format(urlTemplate, startIndex); client.DownloadStringAsync(new Uri(url)); client.Dispose(); }
调用:
StringBuilder sb = new StringBuilder();
DownloadAsync(sb, 1, 10, () => { this.Dispatcher.Invoke(() => { MessageBox.Show(string.Format("下载完成!共{0}字节", sb.ToString().Length)); }); });
我们可以看到为了实现异步操作功能,我们的代码一下变得复杂许多,由于是异步,导致DownloadStringCompleted事件将我们的代码一分为二,下载完成后的代码只能通过Delegate传入,而且出于跨线程考虑还要Dispatcher.Invoke来回到界面线程中执行。性能是提升了,但是代码的可读性完全没有同步实现方式那么简洁。
3. 使用.Net 4.5新方法实现异步
.Net4.5中的新关键字async和await就是为了解决示例2中遇到的问题。请看如下代码:
public async Task<string> DownloadTaskAsync() { StringBuilder sb = new StringBuilder(); for (int i = 1; i <= 10; ++i) { using (WebClient client = new WebClient()) { string url = string.Format(urlTemplate, i); string html = await client.DownloadStringTaskAsync(url); sb.Append(html); } } return sb.ToString(); }
调用:
string content = await DownloadTaskAsync(); MessageBox.Show(string.Format("下载完成!共{0}字节", content.Length));
我们可以看到实现方式基本和同步方式一样,就是多了async, await, Task<string>几处变化。
await client.DownloadStringTaskAsync(url);
通过异步执行网络下载并且线程切换到界面线程然后阻塞等待下载完成,此时界面不会出现假死状态。
4. 将所有异步操作通过一步同时执行
public async Task<string> DownloadAllTaskAsync() { List<Task<string>> tasks = new List<Task<string>>(); for (int i = 1; i <= 10; ++i) { using (WebClient client = new WebClient()) { string url = string.Format(urlTemplate, i); Task<string> task = client.DownloadStringTaskAsync(url); tasks.Add(task); } } StringBuilder sb = new StringBuilder(); string[] result = await Task.WhenAll<string>(tasks); foreach (string each in result) { sb.Append(each); } return sb.ToString(); }
先将异步操作加入队列,然后通过Task.WhenAll一次执行。
5. 新特性中的异常处理
public async Task<string> DownloadExceptionTaskAsync() { StringBuilder sb = new StringBuilder(); using (WebClient client = new WebClient()) { for (int i = 1; i <= 10; ++i) { string url = string.Format(@"http://www.abc.com/{0}", i); try { string html = await client.DownloadStringTaskAsync(url); sb.Append(html); } catch { } } } return sb.ToString(); }
我们可以看到,使用4.5的新特性后,原先的异常处理并没有受到影响。
通过以上的测试我们可以看到,4.5的这个新特性给我们带来的最大好处是,在不影响我们原先代码逻辑的前提下实现了异步,先归还了界面线程的控制权,然后再阻塞等待I/O操作,当然,如果不想阻塞等待结果,我们可以不使用await关键字。
6. 扩展应用
如果不使用.Net框架中的方法,我们如何使用新特性呢?下面就举例看看:
public async Task DoWhileAsync() { await Task.Run(() => { int id = Thread.CurrentThread.ManagedThreadId; long result = 0; int i = 0; while (i < Int32.MaxValue) { result += i; ++i; } }); }
调用:
await WebHelper.DoWhileAsync(); int id = Thread.CurrentThread.ManagedThreadId; MessageBox.Show(string.Format("循环结束!"));
通过Task.Run我们将CPU运算较大的处理放到线程池中执行,然后await。
7. 总结
在使用新特性的时候我们需要注意如下几点:
a. 在使用await关键字的时候,方法需要加上async修饰符。
b. 带有async修饰符的方法可以返回void,Task,Task<T>,T表示await操作的返回值(假设async Task<string> Do();那么await Do();的返回值是string)。
c. 使用await操作时,后台并不会创建额外的线程,它是通过线程切换(GOTO)来实现的。
d. 只有实现了GetAwaiter扩展方法才可以对这个方法进行await操作。
e. 带有async修饰符的方法,方法名后尽量加上Async后缀,比如MyMethodAsync或者MyMethodTaskAsync,用于区分这是异步方法。
8. 代码:AsynchronousExample.7z