一起谈.NET技术,.NET中的异步编程(二)- 传统的异步编程

  在上一篇文章中,我们从构建响应灵敏的界面以及构建高可伸缩性的服务应用来讨论我们为什么需要异步编程,异步编程能给我们带来哪些好处。那么知道了好处,我们就开始吧,但是在异步编程这个方面,说总是比做简单。套用那句不是名言的名言:编写异步程序是困难的,编写可靠的异步程序尤其困难。因为异步程序非常难以编写,而且非常容易出错,很多基本的构造元素在异步编程中都无法使用,这让我们这些开发人员更愿意编写同步的代码,虽然我们知道有些地方真的应该使用异步。

  如何实现异步

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

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


private void btnDownload_Click(object sender,EventArgs e)
{
var request = HttpWebRequest.Create("http://www.sina.com.cn");
var response = request.GetResponse();
var stream = response.GetResponseStream();
using(StreamReader reader = new StreamReader(stream))
{
var content = reader.ReadToEnd();
this.txtContent.Text = content;
}
}

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

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

  使用独立的线程实现异步

  如是你写下了下面的代码:


private void btnDownload_Click(object sender,EventArgs e)
{
var downloadThread = new Thread(Download);
downloadThread.Start();
}

private void Download()
{
var request = HttpWebRequest.Create("http://www.sina.com.cn");
var response = request.GetResponse();
var stream = response.GetResponseStream();
using(StreamReader reader = new StreamReader(stream))
{
var content = reader.ReadToEnd();
this.txtContent.Text = content;
}
}

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

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


private void btnDownload_Click(object sender,EventArgs e)
{
ThreadPool.QueueUserWorkItem((state) => {Download();});
}

private void Download()
{
var request = HttpWebRequest.Create("http://www.sina.com.cn");
var response = request.GetResponse();
var stream = response.GetResponseStream();
using(StreamReader reader = new StreamReader(stream))
{
var content = reader.ReadToEnd();
this.txtContent.Text = content;
}
}

  嗯,很容易完成了。你都有点佩服自己了,这么短的时间居然连线程池这么“高级的技术”都给使用上了。就在你沾沾自喜的时候,你的一个同事走过来说:你这种实现方式是非常低效的,这里要进行的耗时操作属于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设计规范》里对异步模式的描述。在这里我会使用这种模式重新实现上面的代码片段:


private static readonly int BUFFER_LENGTH = 1024;
private void btnDownload_Click(object sender,EventArgs e)
{
var request = HttpWebRequest.Create("http://www.sina.com.cn");
request.BeginGetResponse((ar) => {
var response = request.EndRequest(ar);
var stream = response.GetResponseStream();
ReadHelper(stream,0);
},null);
}

private void ReadHelper(Stream stream,int offset)
{
var buffer = new byte[BUFFER_LENGTH];
stream.BeginRead(buffer,offset,BUFFER_LENGTH,(ar) =>{
var actualRead = stream.EndRead(ar);

if(actualRead == BUFFER_LENGTH)
{
var partialContent = Encoding.Default.GetString(buffer);
Update(partialContent);
ReadHelper(stream,offset+BUFFER_LENGTH);
}
else
{
var latestContent = Encoding.Default.GetString(buffer,0,actualRead);
Update(latestContent);
stream.Close();
}
},null);
}

private void Update(string content)
{
this.BeginInvoke(new Action(()=>{this.txtContent.Text += content;}));
}

  感谢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)进行搜索

时间: 2024-09-13 18:17:15

一起谈.NET技术,.NET中的异步编程(二)- 传统的异步编程的相关文章

一起谈.NET技术,微软缘何认为VB与C#需要异步语法

在过去几年间,多线程编程已经成为了一个热门话题.虽然我们长久以来一直都希望能有高速响应的用户界面,但实现这个愿望的工具却迟迟不见踪迹.对于大多数框架(包括.NET程序员所使用的那些框架)来说,对用户界面的更新仍然局限于单独一个线程,同时,硬件制造商已经转向了多核来代替更快的CPU. C#与VB一开始提供了非常简单的并发支持,这是通过对监视器与委托使用lock/SyncLock关键字来实现的,异步程序库通过这两个关键字实现异步编程.在随后的几个版本中,我们并没有看到这两种语言在异步领域有任何进展,

一起谈.NET技术,中软面试题-最新

      中软的面试比较经典,也比较严格,一般有四轮,类似于微软的面试.中软面过以后,根据项目组,会推到美国微软那边运用live meeting & con-call 再面一次.以下是我的面试题及个人的小分析,拿出来和大家share一下.希望更多的人能过这个坎.如有什么问题,可以一起交流.直接进入主题:  1. English communication. (sale yourself, project information, your interesting,and how to deal

一起谈.NET技术,页面片段缓存(二)

在上一篇文章中,我介绍了我们用土法炼钢的方法,使用Velocity提供的自定义标签实现片段缓存.这样的方式虽然也解决了我们的问题,但还是引出了一些bug.而且还有点hack的味道(虽然我喜欢hack).实际上对于片段缓存,业界有成熟的解决方案,还有一个所谓的W3C标准:ESI(Edge Side Include) . ESI本身没有什么,只是一个XML的标签集合.ESI和SSI(Server Side Include)很相似,做过ASP开发的都熟悉这么一个标签: <!--#include src

一起谈.NET技术,ASP.NET应用下基于SessionState的“状态编程框架”解决方案

在一个基于ASP.NET的Web应用程序中,我们通常使用SessionState保存基于某个客户端的状态信息.但是这种单纯使用SessionState的编程方式具有很多局限,比如Session Item的Key值冲突,比如没有一个有效的SessionState清除机制会为Web Server带来内存压力.为了实现对客户端状态的有效管理,并提高应用开发效率,在很多年前我们的开发框架体系中就具有相应的一个叫做State的编程框架.最近我开始对其进行升级和重新设计,将实现原理和概要设计方面的东西写出来

一起谈.NET技术,.NET中的异步编程(一)-为什么需要异步

在2010年的PDC上,微软发布了Visual Studio Async CTP,大大地降低了异步编程的难度,让我们可以像写同步的方法那样去编写异步代码.Async CTP也在社区里掀起了不小的波澜.在这之后,我也学习了一段时间,这个系列会将这段时间的学习作个梳理. 好了,下面进入本文的正题. 为什么需要异步编程 既然同步的写法更自然简单,异步的代码(传统的)不好写,还容易出错,那我们为什么需要去编写异步的代码呢?微软还要费这么大劲投入对Async CTP的开发呢?这其中肯定有一些原因. 快速响

一起谈.NET技术,.NET中的异步编程-Continuation passing style以及使用yield实现异步

传统的异步方式将本来紧凑的代码都分成两部分,不仅仅降低了代码的可读性,还让一些基本的程序构造无法使用,所以大部分开发人员在遇到应该使用异步的地方都忍痛割爱.本来我在本篇文章中想讨论一下.NET世界中已有的几个辅助异步开发的类库,但是经过思考后觉得在这之前介绍一下一些理论知识也许对理解后面的类库以及更新的内容有所帮助.今天我们要讨论的是Continuation Passing Style,简称CPS. CPS 首先,我们看看下面这个方法: 1: public int Add(int a, int

一起谈.NET技术,从.NET中委托写法的演变谈开去(上):委托与匿名方法

在<关于最近面试的一点感想>一文中,Michael同学谈到他在面试时询问对方"delegate在.net framework1.1,2.0,3.5各可以怎么写"这个问题.于是乎,有朋友回复道"请问楼主,茴香豆的茴有几种写法","当代孔乙己",独乐,众乐.看了所有的评论,除了某些朋友认为"的确不该不知道这个问题"之外,似乎没有什么人在明确支持楼主. 不过我支持,为什么?因为我也提过出这样的问题. 这样,我们暂且不提应

一起谈.NET技术,从.NET中委托写法的演变谈开去(中):Lambda表达式及其优势

在上一篇文章中我们简单探讨了.NET 1.x和.NET 2.0中委托表现形式的变化,以及.NET 2.0中匿名方法的优势.目的及注意事项.那么现在我们来谈一下.NET 3.5(C# 3.0)中,委托的表现形式又演变成了什么样子,还有什么特点和作用. .NET 3.5中委托的写法(Lambda表达式) Lambda表达式在C#中的写法是"arg-list => expr-body","=>"符号左边为表达式的参数列表,右边则是表达式体(body).参数列表

浅谈在JAVA项目中LOG4J的使用_java

一.直接使用: //输出到项目文件夹下output1.txt文件中 ////////////////////////////// // DEBUG - Here is some DEBUG // INFO - Here is some INFO // WARN - Here is some WARN // ERROR - Here is some ERROR // FATAL - Here is some FATAL ////////////////////////////// package

《创业家》牛文文:少谈点模式多谈点技术

"模式"如同当年的"主义",流行于各种创业大赛.创业励志节目.论坛的"街头"式秀场 文/创业家 牛文文 "美国某某公司你知道吧?就是刚被戴尔.惠普.思科十几亿美元抢购的那家.我们的模式和它的一样,现在还没赢利,可将来起码有十几亿人民币的市值." "我开了小煤矿,但煤运不出去,上商学院之后受到启发,想搞模式创新,具体讲就是想在铁路边上搞个煤炭物流开发区,建一个大的物流和信息流平台,把分散的煤炭集中在我这个园区,这样和铁