【www.gdgbn.com--日常祝福】

asp教程.net如何实现异步编程
对于很多人来说,异步就是使用后台线程运行耗时的操作。在有些时候这是对的,而在我们日常大部分场景中却不对。

比如现在我们有这么一个需求:使用httpwebrequest请求某个指定uri的内容,然后输出在界面上的文本域中。同步代码很容易编写:

   1: private void btndownload_click(object sender,eventargs e)   2: {   3:     var request = httpwebrequest.create("http://www.sina.com.cn");   4:     var response = request.getresponse();   5:     var stream = response.getresponsestream();   6:     using(streamreader reader = new streamreader(stream))   7:     {   8:         var content = reader.readtoend();   9:         this.txtcontent.text = content;  10:     }  11: }


是吧,很简单。但是正如上一篇文章所说,这个简短的程序体验会非常差。特别是在uri所指向的资源非常大,网络非常慢的情况下,在点击下载按钮到获得结果这段时间界面会假死。

哦,这个时候你想起了异步。回忆上篇文章的示意图。我们发现只要我们将耗时的操作放到另外一个线程上执行就可以了,这样我们的ui线程可以继续响应用户的操作。

使用独立的线程实现异步
如是你写下了下面的代码:

 

  1: private void btndownload_click(object sender,eventargs e)   2: {   3:     var downloadthread = new thread(download);   4:     downloadthread.start();   5: }   6:     7: private void download()   8: {   9:     var request = httpwebrequest.create("http://www.sina.com.cn");  10:     var response = request.getresponse();  11:     var stream = response.getresponsestream();  12:     using(streamreader reader = new streamreader(stream))  13:     {  14:         var content = reader.readtoend();  15:         this.txtcontent.text = content;  16:     }  17: }

然后,f5运行。很不幸,这里出现了异常:我们不能在一个非ui线程上更新ui的属性(更详细的讨论参见我的这篇文章:winform二三事(三)control.invoke&control.begininvoke)。我们暂时忽略这个异常(在release模式下是不会出现的,但这是不推荐的做法)。

哦,你写完上面的代码后发现ui不再阻塞了。心里想,异步也不过如此嘛。过了一会儿你突然想起,你好像在哪本书里看到过说尽量不要自己声明thread,而应用使用线程池。如是你搜索了一下msdn,将上面的代码改成下面这个样子:

  

1: private void btndownload_click(object sender,eventargs e)   2: {   3:     threadpool.queueuserworkitem(download);       4: }   5:     6: private void download()   7: {   8:     var request = httpwebrequest.create("http://www.sina.com.cn");   9:     var response = request.getresponse();  10:     var stream = response.getresponsestream();  11:     using(streamreader reader = new streamreader(stream))  12:     {  13:         var content = reader.readtoend();  14:         this.txtcontent.text = content;  15:     }  16: }
 

嗯,很容易完成了。你都有点佩服自己了,这么短的时间居然连线程池这么“高级的技术”都给使用上了。就在你沾沾自喜的时候,你的一个同事走过来说:你这种实现方式是非常低效的,这里要进行的耗时操作属于io操作,不是计算密集型,可以不分配线程给它(虽然不算准确,但如果不深究的话就这么认为吧)。

你的同事说的是对的。对于io操作(比如读写磁盘,网络传输,数据库教程查询等),我们是不需要占用一个thread来执行的。现代的磁盘等设备,都可以与cpu同时工作,在磁盘寻道读取这段时间cpu可以干其他的事情,当读取完毕之后通过中断再让cpu参与进来。所以上面的代码,虽然构建了响应灵敏的界面,但是却创建了一个什么也不干的线程(当进行网络请求这段时间内,该线程会被一直阻塞)。所以,如果你要进行异步时首先要考虑,耗时的操作属于计算密集型还是io密集型,不同的操作需要采用不同的策略。对于计算密集型的操作你是可以采用上面的方法的:比如你要进行很复杂的方程的求解。是采用专门的线程还是使用线程池,也要看你的操作的关键程度。

这个时候你又在思考,不让我使用线程,又要让我实现异步。这该怎么办呢?微软早就帮你想到了这点,在.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设计规范》里对异步模式的描述。在这里我会使用这种模式重新实现上面的代码片段:

   1: private static readonly int buffer_length = 1024;   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方法的命名,参数的传递都是有一定规则的,弄清楚这些规则在进行异步编程时会事半功倍)

 

本文来源:http://www.gdgbn.com/zhufuduanxin/29208/