这个时候你又在思考,不让我使用线程,又要让我实现异步。这该怎么办呢?微软早就帮你想到了这点,在.NET Framework中,几乎所有进行IO操作的方法几乎都提供了同步版本和异步版本,而且微软为了简化异步的使用难度还定义了两种异步编程模式:
Classic Async Pattern
这种方式就是提供两个方法实现异步编程:比如System.IO.Stream的Read方法:
public int Read(byte[] buffer,int offset,int count);
它还提供了两个方法实现异步读取:
public IAsyncResult BeginRead(byte[] buffer, int offset,int count,AsyncCallback callback);
public int EndRead(IAsyncResult asyncResult);
以Begin开头的方法发起异步操作,Begin开头的方法里还会接收一个AsyncCallback类型的回调,该方法会在异步操作完成后执行。然后我们可以通过调用EndRead获得异步操作的结果。关于这种模式更详细的细节我不在这里多阐述,感兴趣的同学可以阅读《CLR via C#》26、27章,以及《.NET设计规范》里对异步模式的描述。在这里我会使用这种模式重新实现上面的代码片段:
2:
3: private void btnDownload_Click(object sender,EventArgs e)
4: {
5: var request = HttpWebRequest.Create("http://www.sina.com.cn");
6: request.BeginGetResponse((ar) => {
7: var response = request.EndRequest(ar);
8: var stream = response.GetResponseStream();
9: ReadHelper(stream,0);
10: },null);
11: }
12:
13: private void ReadHelper(Stream stream,int offset)
14: {
15: var buffer = new byte[BUFFER_LENGTH];
16: stream.BeginRead(buffer,offset,BUFFER_LENGTH,(ar) =>{
17: var actualRead = stream.EndRead(ar);
18:
19: if(actualRead == BUFFER_LENGTH)
20: {
21: var partialContent = Encoding.Default.GetString(buffer);
22: Update(partialContent);
23: ReadHelper(stream,offset+BUFFER_LENGTH);
24: }
25: else
26: {
27: var latestContent = Encoding.Default.GetString(buffer,0,actualRead);
28: Update(latestContent);
29: stream.Close();
30: }
31: },null);
32: }
33:
34: private void Update(string content)
35: {
36: this.BeginInvoke(new Action(()=>{this.txtContent.Text += content;}));
37: }
感谢lambda表达式,让我少些了很多方法声明,也少引入了很多实例成员。不过上面的代码还是非常难以读懂,原本简简单单的同步代码被改写成了分段式的,而且我们再也无法使用using了,所以需要显示的写stream.Close()。哦,我的代码还没有进行异常处理,这令我非常头痛。实际上要写出一个健壮的异步代码是非常困难的,而且非常难以调试。但是,上面的代码不仅仅能创建响应灵敏的界面,还能更高效的利用线程。在这种异步模式中,BeginXXX方法会返回一个IAsyncResult对象,在进行异步编程时也非常有效,关于它的更详细信息你可以阅读我的这篇文章:WinForm二三事(二)异步操作。
除此之外,因为我们在这里不能使用while等循环,我们想要从stream里读取完整的内容并不是一件容易事儿,我们必须将很好的循环结果替换成递归调用:ReadHelper。
Event-based Async Pattern(EAP)
.NET Framework除了提供上面这种编程模式外,还提供了基于事件的异步编程模式。比如WebClient的很多方法就提供了异步版本,比如DownloadString方法。
同步版本:
public string DownloadString(string url);
异步版本:
public void DownloadStringAsync(string url);
public event DownloadStringCompleteEventHandler DownloadStringComplete;
(在这里请注意,这两种异步编程模式以及未来要介绍的Async CTP中的TAP方法的命名,参数的传递都是有一定规则的,弄清楚这些规则在进行异步编程时会事半功倍)
基于事件的异步模式我也不作过多阐述,同样可以参考《CLR via C#》以及MSDN。基于事件的异步编程模式点相比上一种的优点是实现了该模式的类一般从Component派生,所以可以获得更好的设计器支持,但如此一来也会在性能上稍微差一点点。
尴尬
虽然微软费尽心思,提出两种异步编程的模式,让我们编写异步代码能稍微轻松那么一点点;但不管是使用回调还是基于事件的异步模式,都会将顺序的同步方式的代码拆成两个部分:一个部分发起异步操作,而另外一个部分获得结果。当有多个异步操作要进行时(比如上面的代码首先使用异步的方式获得response,然后又使用异步的方式读取stream中的内容)就会回调里嵌套着另外一个异步调用,代码更加混乱。而且方法打散之后,像using、for、while、常规的异常处理都变得难以进行。代码的可读性也急剧降低,代码又容易出错,如是我们舍尔求其次,转而去使用低效的同步版本。
不过作为.NET程序员我们是幸运的,因为.NET提供的一些特性让我们可以开发一些类库辅助异步开发,比如Jeffrey Richter的AsyncEnumerator,以及微软的CCR。我们会在接下来的文章里讨论这些第三方类库的使用以及背后的原理。
最后还是套用Async CTP的程序经理Lucian Wischik的那句话:异步并不意味着后台线程结束本文。
参考文献
《CLR via C#》
关于IO部分,如果想更深入了解,可以使用IO完成端口(或对应英文IO Completion Port)进行搜索