最好的理解这种问题的方式是们最常见的一种情况:用户界面只拥有一个线程所有的工作都运行在这个线程上,客户端程序不能对用户的鼠标时间做出反 应,这很可能是因为应用程序正在被一个耗时的操作所阻塞,这可能是因为线程在等待一个网络ID或者在做一个CPU密集型的计算,此时用户界面不能获得运行 时间,程序一直处于繁忙的状态,这是一个非常差的用户体验。
当们处理一些长线的调用时,经常会导致界面停止响应或者IIS线程占用过多等问题,这个时候们需要更多的是用异步编程来修正这些问题,但是通常 都是说起来容易做起来难,诚然异步编程相对于同步编程来说,它是一种完全不同的编程思想,对于习惯了同步编程的开发者来说,在开发过程中难度更大,可控性 不强是它的特点。
在.NET Framework5.0种,微软为们系统了新的语言特性,让们使用异步编程就像使用同步编程一样相近和简单,本文中将会解释以前版本的Framework中基于回调道德异步编程模型的一些限制以及新型的API如果让们简单的做到同样的开发任务。
为什么要异步
一直以来,使用远程资源的编程都是一个容易造成困惑的问题,不同于 本地资源 ,远程资源的访问总会有很多意外的情况,网络环境的不稳定机器服务端的故障,会造成很多程序员完 全不可控的问题,所以这也就要求程序员需要更多的去保护远程资源的调用,管理调用的取消、超市、线程的等待以及处理线程长时间没响应的情况等。而 在.NET中们通常忽略了这些挑战,事实上们会有多种不用的模式来处理异步编程,比如在处理IO密集型操作或者高延迟的操作时候不组测线程,多数情况 们拥有同步和异步两个方法来做这件事。可是问题在于当前的这些模式非常容易引起混乱和代码错误,或者开发人员会放弃然后使用阻塞的方式去开发。
而在如今的.NET中,提供了非常接近于同步编程的编程体验,不需要开发人员再去处理只会在异步编程中出现的很多情况,异步调用将会是清晰的且不透明的,而且易于和同步的代码进行组合使用。
过去糟糕的体验
最好的理解这种问题的方式是们最常见的一种情况:用户界面只拥有一个线程所有的工作都运行在这个线程上,客户端程序不能对用户的鼠标时间做出反 应,这很可能是因为应用程序正在被一个耗时的操作所阻塞,这可能是因为线程在等待一个网络ID或者在做一个CPU密集型的计算,此时用户界面不能获得运行 时间,程序一直处于繁忙的状态,这是一个非常差的用户体验。
很多年来,解决这种问题的方法都是做异步花的调用,不要等待响应,尽快的返回请求,让其他事件可以同时执行,只是当请求有了最终反馈的时候通知应用程序让客户代码可以执行指定的代码。
而问题在于:异步代码完全毁掉了代码流程,回调代理解释了之后如何工作,但是怎么在一个while循环里等待?一个if语句?一个try块或者一个using块?怎么去解释 接下来做什么 ?
看下面的一个例子:
public int SumPageSizes(IList Uri uris) { int total = 0; foreach (var uri in uris) { txtStatus.Text = string.Format( Found {0} bytes... , total); var data = new WebClient().DownloadData(uri); total += data.Length; } txtStatus.Text = string.Format( Found {0} bytes total , total); return total; }
这个方法从一个uri列表里下载文件,统计他们的大小并且同时更新状态信息,很明显这个方法不属于UI线程因为它需要花费非常长的时间来完成,这样它会完全的挂起UI,但是们又希望UI能被持续的更新,怎么做呢?
们可以创建一个后台编程,让它持续的给UI线程发送数据来让UI来更新自身,这个看起来是很浪费的,因为这个线程把大多时间花在等下和下载上,但 是有的时候,这正是们需要做的。在这个例子中,WebClient提供了一个异步版本的DownloadData方法 DownloadDataAsync,它会立即返回,然后在DownloadDataCompleted后触发一个事件,这允许用户写一个异步版本的方法 分割所要做的事,调用立即返回并完成接下来的UI线程上的调用,从而不再阻塞UI线程。下面是第一次尝试:
public void SumpageSizesAsync(IList Uri uris) { SumPageSizesAsyncHelper(uris.GetEnumerator(), 0); } public void SumPageSizesAsyncHelper(IEnumerator Uri enumerator, int total) { if (enumerator.MoveNext()) { txtStatus.Text = string.Format( Found {0} bytes... , total); var client = new WebClient(); client.DownloadDataCompleted += (sender,e)= { SumPageSizesAsyncHelper(enumerator, total + e.Result.Length); }; client.DownloadDataAsync(enumerator.Current); } else { txtStatus.Text = string.Format( Found {0} bytes total , total); } }
然后这依然是糟糕的,们破坏了一个整洁的foreach循环并且手动获得了一个enumerator,每一个调用都创建了一个事件回调。代码用递归取代了循环,这种代码你应该都不敢直视了吧。不要着急,还没有完 。
原始的代码返回了一个总数并且显示它,新的一步版本在统计还没有完成之前返回给调用者。们怎么样才可以得到一个结果返回给调用者,答案是:调用者必须支持一个回掉,们可以在统计完成之后调用它。
然而异常怎么办?原始的代码并没有关注异常,它会一直传递给调用者,在异步版本中,们必须扩展回掉来让异常来传播,在异常发生时,们不得不明确的让它传播。
最终,这些需要将会进一步让代码混乱:
public void SumpageSizesAsync(IList Uri uris,Action int,Exception callback) { SumPageSizesAsyncHelper(uris.GetEnumerator(), 0, callback); } public void SumPageSizesAsyncHelper(IEnumerator Uri enumerator, int total,Action int,Exception callback) { try { if (enumerator.MoveNext()) { txtStatus.Text = string.Format( Found {0} bytes... , total); var client = new WebClient(); client.DownloadDataCompleted += (sender, e) = { SumPageSizesAsyncHelper(enumerator, total + e.Result.Length,callback); }; client.DownloadDataAsync(enumerator.Current); } else { txtStatus.Text = string.Format( Found {0} bytes total , total); enumerator.Dispose(); callback(total, null); } } catch (Exception ex) { enumerator.Dispose(); callback(0, ex); } }
当你再看这些代码的时候,你还能立马清楚的说出这是什么JB玩意吗?
恐怕不能,们开始只是想和同步方法那样只是用一个异步的调用来替换阻塞的调用,让它包装在一个foreach循环中,想想一下试图去组合更多的异步调用或者有更复杂的控制结构,这不是一个SubPageSizesAsync的规模能解决的。
们的真正问题在于们不再可以解释这些方法里的逻辑,们的代码已经完全无章可循。异步代码中很多的工作让整件事情看起来难以阅读并且似乎充满了BUG。