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