当前位置:编程学习 > 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((state) => {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操作,不是计算密集型,可以不分配线程给它(虽然不算准确,但如果不深究的话就这么认为吧)。

BeginInvoke & EndInvoke

.NET里委托还提供了另外两个方法:BeginInvoke和EndInvoke用来实现异步。其实这种实现方式跟使用线程来实现是类似的,都会占用CPU时间,如果遇到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)
  14: {
  15:         var buffer = new byte[BUFFER_LENGTH];
  16:         stream.BeginRead(buffer,0,BUFFE

补充:Web开发 , ASP.Net ,
CopyRight © 2022 站长资源库 编程知识问答 zzzyk.com All Rights Reserved
部分文章来自网络,