.N“.NET研究”ET中的异步编程(二)- 传统的异步编程

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

  如何实现异步

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

  比如现在我们有这么一个需求:使用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) =>上海徐汇企业网站设计与制作pan>{
var actualRead = stream.EndRead(ar);

if(actualRead ==上海闵行企业网站设计与制作tyle="color: #000000;"> 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上海企业网站制作 style="color: #000000;">.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)进行搜索

时间: 2025-01-25 05:17:42

.N“.NET研究”ET中的异步编程(二)- 传统的异步编程的相关文章

Windows Socket网络编程(二) 套接字编程原理

一.客户机/服务器模式 在TCP/IP网络中两个进程间的相互作用的主机模式是客户机/服务器模式(Client/Server model).该模式的建立基于以下两点:1.非对等作用:2.通信完全是异步的.客户机/服务器模式在操作过程中采取的是主动请示方式: 首先服务器方要先启动,并根据请示提供相应服务:(过程如下) 1.打开一通信通道并告知本地主机,它愿意在某一个公认地址上接收客户请求. 2.等待客户请求到达该端口. 3.接收到重复服务请求,处理该请求并发送应答信号. 4.返回第二步,等待另一客户

.NET中的“.NET研究”异步编程:使用F#简化异步编程

不管是使用yield或借助第三方类库来简化异步编程,或多或少总是感觉不那么正统,有点hack的感觉.这种感觉在实验阶段倒还可以,要是用在产品中总有点担心,即使这些类库来自权威的第三方,我不知道大家有没有跟我同样的感觉.那么这个时候我们就会想,如果在语言中直接能提供这种机制该多好呢. F#的异步工作流 在Visual Studio 2010中,新包含了一种语言:F#.F#的一大特性就是异步计算.能让你用同步的方式编写异步的代码,不用使用AsyncCallback回调将一个方法分为两段,也不用注册异

.“.NET研究”NET中的异步编程(一)-为什么需要异步

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

研究 Java 中 XML 文档模型的特性和性能

xml|性能 Java 中的 XML: 文档模型,第一部分:性能 研究 Java 中 XML 文档模型的特性和性能 文档选项 将此页作为电子邮件发送 最新推荐 Java 应用开发源动力 - 下载免费软件,快速启动开发 级别: 初级 Dennis M. Sosnoski, 总裁, Sosnoski Software Solutions, Inc. 2001 年 9 月 01 日 在本文中,Java 顾问 Dennis Sosnoski 比较几个 Java 文档模型的性能和功能.当选择模型时,无法做

拿来主义往往束缚人们对新事物的研究与发现 - 记于 OpenGLES 模型移动研究过程中的感悟

拿来主义往往束缚人们对新事物的研究与发现 - 记于 OpenGLES 模型移动研究过程中的感悟 太阳火神的美丽人生 (http://blog.csdn.net/opengl_es) 本文遵循"署名-非商业用途-保持一致"创作公用协议 转载请保留此句:太阳火神的美丽人生 -  本博客专注于 敏捷开发及移动和物联设备研究:iOS.Android.Html5.Arduino.pcDuino,否则,出自本博客的文章拒绝转载或再转载,谢谢合作. OpenGLES 在 iOS 上的研究工作已经持续

Python中利用原始套接字进行网络编程的示例

  这篇文章主要介绍了Python中利用原始套接字进行网络编程的示例,使用sock_raw接受和发送数据包可以避开网络协议的诸多限制,需要的朋友可以参考下 在实验中需要自己构造单独的HTTP数据报文,而使用SOCK_STREAM进行发送数据包,需要进行完整的TCP交互. 因此想使用原始套接字进行编程,直接构造数据包,并在IP层进行发送,即采用SOCK_RAW进行数据发送. 使用SOCK_RAW的优势是,可以对数据包进行完整的修改,可以处理IP层上的所有数据包,对各字段进行修改,而不受UDP和TC

将数组元素按顺序放入链表中并进行插入删除等操作的编程问题

问题描述 将数组元素按顺序放入链表中并进行插入删除等操作的编程问题 编译无错误,但是无法运行,感觉是将数组当做参数那个地方出了问题,但是不知道具体原因,求助CSDN的朋友帮忙解答,万分感谢 解决方案 大体看了一下首先在list的构造函数中 没有对head进行初始化, 应该是 head = new Node; head->next = null;其次在你的create方法中你并没有将数据放入到以head为头的链表中,我猜你应该是忘记对q初始化,q = head; 给你的建议是加断点一步步调试, 这

VB编程 及EXCEL 的VBA编程,用什么把一段代码括起来啊(就是用什么东西来实现C语言中的{}功能啊)?

问题描述 VB编程及EXCEL的VBA编程,用什么把一段代码括起来啊(就是用什么东西来实现C语言中的{}功能啊)? 解决方案 解决方案二:不是有begin和end吗解决方案三:region?C的{}有很多啊,只能你VB书都没看过if...endif-------------if{}for...endfor---------for{}解决方案四:for..next.............我草解决方案五:学c的时候用按键精灵的时候我也愣了一阵子...很多是用end,if之后用endif,while

vc++ 设备上下文-关于VC++中dc设备上下文的理解,文本编程

问题描述 关于VC++中dc设备上下文的理解,文本编程 在孙鑫老师的VC++视频教程中学习文本编程看到下面的代码: if (0x08 == nChar) { COLORREF clr = dc.SetTextColor (dc.GetBkColor ()); dc.TextOut (m_ptOrigin.x ,m_ptOrigin.y ,m_strLine); m_strLine = m_strLine.Left (m_strLine.GetLength ()-1); dc.SetTextCol

Java Web项目中连接Access数据库的配置方法_JSP编程

老师决定期末考试采用access数据库实现增删改查,我认为现在的我已经没有问题了,但是以前都是在JSP页面中连接access数据库,无论是以下的那种方式都进行了连接的练习,但是现在我想让我的项目中的访问access数据库的java代码,封装到DAO中,在DAO中连接数据库,没有和Servlet API有任何的关系.对于大多数人都会优先选择使用ODBC数据源的方式或者是使用绝对路径的方式连接access数据库,但是我个人认为,这样做不太好,如果采用这样的方式,项目做好后,放到他人的服务器上是无法运