Spiga

正确使用异步操作

2008-02-24 22:03 by Jeffrey Zhao, 20569 visits, 网摘, 编辑

  本想写一点有关LINQ to SQL异步调用的话题,但是在这之前我想还是先写一篇文章来阐述一下使用异步操作的一些原则,避免有些朋友误用导致程序性能反而降低。这篇文章会讨论一下在.NET中有关异步操作话题,从理论出发结合实际,以澄清概念及避免误用为目标,并且最后提出常见的异步操作场景和使用案例。这样我们就可以知道什么时候该使用异步操作,什么时候会得不偿失。

  那么我们先来确认一个概念,那就是“线程”。请注意,如果没有特殊说明,本文中出现的“线程”所指的是CLR线程池(Thread Pool)中的托管线程,它和Windows线程或纤程(fiber)并不是同一个的概念。同样,它也不是指System.Thread类的实例。简单地说,它是由CLR管理的工作执行单元,每当需要执行任务时,CLR就会分配一个这样的执行单元去工作。当所有的线程池内的线程都用完之后就无法执行新的任务了,一个托管线程在任务完成之后被释放为止。线程池本身是一个“对象池”,会在需要新对象(托管线程)时创建,而在对象不需要之后(一段特定时间之内没有新任务需要分配托管线程)负责销毁以释放资源。至于线程池的线程数量,在CLR 2.0 SP1之前的版本中是CPU数 * 25,不过从CLR 2.0 SP1之后就变成了CPU数 * 250。不过不管怎么样,线程池内的线程是有限的,我们必须合理地使用它。

  以前的计算机只有一个CPU,理论上同一时刻只能执行一个任务。而如今的超线程、多核、甚至是真正的多个CPU都使计算机能够同时运行多个任务。多线程编程的一个重要特点就是能够充分利用CPU的运算能力,更快地完成某个任务。很明显,如果一个非常庞大的计算任务只交由一个线程来完成,那么只能让一个CPU参与运算。但是如果将一个大任务拆分成多个互不影响的子任务,那么就能让多个CPU同时参与运算,所花的时间自然就少了。如果某个操作的目的是进行大量运算,或者说需要花费大量时间运算上的操作,我们将其称作“Compute-Bound Operation”,也就是受运算能力限制的操作。

  与“Compute-Bound Operation”相对的则是“IO-Bound Operation”。“IO-Bound Operation”是指那些由于受到外部条件限制,完成这样一个任务需要在IO上花费大量时间的操作。例如读取一个文件,或者请求网络上的某个资源。对于这种操作,计算的线程再多,运算能力再强也无济于事,因为任务受到的是硬盘、网络等IO设备带来的限制。对于IO-Bound Operation,我们能做的只有“等待”。

  对于“同步操作”来说,“等待”就意味着“阻塞”,一个线程将会“无所事事”直至操作完成。这种做法在许多时候会带来各种问题,因此就出现了“异步操作”,但是同样是“异步操作”,不同的任务,不同的情况,它解决问题的方式和带来的效果也是不同的。我下面就通过生活中的实例来说明这些内容:

  老赵的朋友开了一家餐馆,请了10个工作人员。最近那个朋友经常向老赵抱怨,说工作人员人手总是不够,在客人比较多的时候,总是来不及招呼他们。老赵一问才得知,这家餐馆的工作方式比较特别:当客人来用餐时,就会有工作人员迎上去热情招待,当客人点好菜之后,工作人员就会去进入厨房亲自下厨——没错,就是这样——做完之后,工作人员会将饭菜端至客人面前,然后就去招待别的客人。因为烧菜往往需要很长时间,因此在某些时候就会发现所有的工作人员都在厨房,但是却没有人点菜。于是老赵给朋友出了个主意:让几个工作人员作为服务员,只负责招呼客人,剩下的就当厨师,一直在厨房工作。当客人点菜之后,服务员就把客人的需求告诉厨师,厨师开始工作,而服务员就可以去招呼其他客人了。朋友顿悟,问题就这样迎刃而解了。

  当然,上面故事中老赵的朋友实在太笨,现实生活中的餐馆老板都不会犯这种人员调度上的低级失误。开发一个客户端应用程序所遇到的情况往往就和以上的情况类似。在运行程序时,UI线程(服务员)负责显示界面(招待客人),当用户操作应用程序(点菜)之后,UI线程可以使用同步操作进行运算(服务员亲自下厨),但是如果这是个长时间的Compute-Bound Operation(烧菜是个花费人手时间较长的操作),界面就无法重绘或响应用户请求了(无法招待客人了),这样的应用程序用户体验自然不好(客人觉得服务质量低下)。但是只要UI线程使用异步操作(通知厨师),让另一个线程(另一个工作人员)来进行运算,UI线程就可以继续负责界面重绘或者其他用户操作(招待其他客人)了。

  在这种的情况下,异步操作并没有提高运算能力或者节省资源(还是需要一个人员的工作),但是提供了较好的用户体验。不过我们这时该怎么利用异步操作呢?在实际开发中,我们可以使用委托的BeginInvoke进行异步调用。

  下面的例子则对应了另一种情况:

  老赵的那个开餐馆的朋友在小赚一笔之后准备再开一家快餐店。快餐店和餐馆有个不同之处,那就是快餐店的食品生产了大都有机器完成。可惜在这种情况下那个朋友还是遇到了问题:机器数量绰绰有余,但是人手还是不够。原来现在的做法还是相当不科学:服务员知道客人需要的食品之后,就将原料塞入机器,并看着机器是如何将原料变为美味的。当机器的工作完成之后,服务员便将食品打包并送出,然后继续招待别的客人。老赵听后还是哭笑不得:为啥服务员不能在机器工作的时候就去招待别的客人呢?

  与这个示例对应的可以是一个ASP.NET应用程序。在ASP.NET中每个请求(客人)都会使用一个线程池内的线程(服务员)来处理(招待),处理中很可能需要访问数据库(使用机器),对于普通的做法,处理线程会等待数据库操作返回(服务员看着机器直至完成)。对于Web服务器来说,这很可能是个长时间的IO-Bound Operation,如果线程长时间被阻塞很可能就会降低Web应用程序的性能,因为线程池里的线程用完之后(服务员都去看炉子了),就无法处理新的请求了(没人招待客人了)。如果我们能够在数据库进行长时间查询操作时,让线程去处理其他的请求(招待其他客人)。这样,我们只需要在数据库操作完成之后继续处理(打包)并将数据发送给客户端(送出)即可。

  这就是处理IO-Bound Operation的方式,很显然,这也是一个异步操作。当我们希望进行一个异步的IO-Bound Operation时,CLR会(通过Windows API)发出一个IRP(I/O Request Packet)。当设备准备妥当,就会找出一个它“最想处理”的IRP(例如一个读取离当前磁头最近的数据的请求)并进行处理,处理完毕后设备将会(通过Windows)交还一个表示工作完成的IRP。CLR会为每个进程创建一个IOCP(I/O Completion Port)并和Windows操作系统一起维护。IOCP中一旦被放入表示完成的IRP之后(通过内部的ThreadPool.BindHandle完成),CLR就会尽快分配一个可用的线程用于继续接下去的任务。

  这种做法的需要一个重要条件,这就是发出用于请求的IRP的操作能够立即返回,并且这个IO操作不会使用任何线程。而此时,这种异步调用是真正地在节省资源,因为我们可以腾出线程用来处理其他任务了,这就是和第一种异步调用的最大区别。不过很可惜,这种做法显然需要操作系统和设备的支持,也就是只有特定的操作才能享受这些待遇。那么.NET Framework中哪些操作能从中获利呢?

  • FileStream操作:BeginRead、BeginWrite。调用BeginRead/BeginWrite时会发起一个异步操作,但是只有在创建FileStream时传入FileOptions.Asynchronous参数才能获取真正的IOCP支持,否则BeginXXX方法将会使用默认定义在Stream基类上的实现。Stream基类中BeginXXX方法会使用委托的BeginInvoke方法来发起异步调用——这会使用一个额外的线程来执行任务。虽然当前调用线程立即返回了,但是数据的读取或写入操作依旧占用着另一个线程(IOCP支持的异步操作时不需要线程的),因此并没有任何“节省”,反而还很有可能降低了应用程序的性能,因为额外的线程切换会造成性能损失。
  • DNS操作:BeginGetHostByName、BeginResolve。
  • Socket操作:BeginAccept、BeginConnect、BeginReceive等等。
  • WebRequest操作:BeginGetRequestStream、BeginGetResponse。
  • SqlCommand操作:BeginExecuteReader、BeginExecuteNonQuery等等。这可能是开发一个Web应用时最常用的异步操作了。如果需要在执行数据库操作时得到IOCP支持,那么需要在连接字符串中标记Asynchronous Processing为true(默认为false),否则在调用BeginXXX操作时就会抛出异常。
  • WebServcie调用操作:例如.NET 2.0或WCF生成的Web Service Proxy中的BeginXXX方法、WCF中ClientBase<TChannel>的InvokeAsync方法。

  有一点我想再强调一下,那就是委托的BeginInvoke方法并不能获得IOCP支持,这会使用一个额外的线程来执行任务,这样不但没有节省,返而会降低性能。还有一点可能需要注意,IOCP的确可以不占用线程,但是一个真正的异步操作也不能毁在我们的代码中。例如我曾经看到过如下的代码:

SqlCommand command;

IAsyncResult ar = command.BeginExecuteNonQuery();
int result = command.EndExecuteNonQuery(ar);

  虽然在调用BeginExecuteNonQuery方法之后的确获得了IOCP的支持,但是之后调用的EndExecuteNonQuery却会阻塞当前线程直至数据库操作返回——异步操作不是这样用的。至于正确的做法,网络上已经有不少文章讲述了如何在ASP.NET中正确使用异步操作,大家可以搜索相应的资料来看,我也会在以后的文章中略有提到。

  关于异步操作,这次就讲到这里吧。

Add your comment

34 条回复

  1. #1楼  Eeyore      2008-02-24 21:56
    学习了~
      回复  引用  查看    
  2. #2楼  jillzhang      2008-02-24 22:18
    慢慢读,老赵刚在我帖子上回复,就过来开个主题。
    先说声对不起了,有些误导兄弟们
    先学习,一会发表感悟
      回复  引用  查看    
  3. #3楼 [楼主] Jeffrey Zhao      2008-02-24 22:48
    @jillzhang
    不要介意,不光是因为看到你帖子的内容,也因为接下去会写点有关这方面的东西。:)
      回复  引用  查看    
  4. #4楼  xingd      2008-02-24 22:57
    对于某些不需要返回信息到当前过程的操作,比如日志处理,可以用ThreadPool.QueueUserWorkItem加入到异步处理队列中。这种情况跟你在此文中讲述的情况不太一样,老赵有什么建议?
      回复  引用  查看    
  5. #5楼  水煮 鱼      2008-02-24 23:02
    “这就是处理IO-Bound Operation的方式,很显然,这也是一个异步操作。当我们希望进行一个异步的IO-Bound Operation时,CLR会(通过Windows API)向设备(例如硬盘)发出IRP(I/O Request Packet)。当设备好准备处理请求时,就会找出一个它“最想处理”的IRP(例如一个读取离当前磁头最近的数据的请求)并进行处理,处理完毕后设备将会交还一个表示工作完成的IRP。CLR会为每个进程创建一个IOCP(I/O Completion Port)并和Windows操作系统一起维护。IOCP中一旦被放入表示完成的IRP之后(通过内部的ThreadPool.BindHandle完成),CLR就会尽快分配一个可用的线程用于继续接下去的任务。”

    可能因为我做过底层的东西,所以对楼主这里这么写觉得不太正确。
    由于目前系统的层次化结构为:
    应用层-系统层(包括了操作系统内核)-驱动层-硬件层
    可能由于系统层通过API方式提供了接口,所以一般上层软件都是能够理解的一些请求和应答是楼主文中写道的IRP,但是实际上,这个IRP仅仅存在于应用层和系统层之间的消息。当一个消息到达系统层,可能会经过一定的转化,转换为驱动层可以理解的命令,驱动层执行该命令会向目的IO设备的寄存器写入相应的命令字。在该文中,就是写入硬盘是否准备好的命令请求。如果准备好,驱动会再次向硬盘写入DMA读取命令(因为目前一般访问方式都为DMA),硬盘执行命令,这段时间也就是楼主提到的数据库访问过程(这个整个过程都假设数据库均保存在硬盘上),这段时间,如果楼主应用软件采用的是异步操作方式,则相应的任务将释放CPU资源。,当DMA操作完成,一般会上报一个DMA中断,这时CPU将响应该终端。驱动则上报完成事件到系统层,系统层则返回楼主文中提到的IRP响应报文,应用层相应的任务再解析响应报文,并回调处理函数,实现整个异步操作过程。

    上面的描述仅仅是一个很简单粗略的描述,其实整个过程的机制可能更复杂,由于一直都做嵌入式,我也是完全根据嵌入式开发的经验,大概描述下上述过程。
      回复  引用  查看    
  6. #6楼  绿蚂蚁      2008-02-24 23:06
    期待下一次~
      回复  引用  查看    
  7. #7楼 [楼主] Jeffrey Zhao      2008-02-24 23:10
    @水煮 鱼
    我的文章里主要是简单地叙述一下这种异步操作在CLR视角中的理解,CLR最多与系统层通讯,因此我最多提及到IRP。至于系统层是如何与驱动层通信对于CLR是隐藏的,因此没有在文章中有太多描述,有可能也不是描述地很妥当。
    多谢关注我的文章,如果再发现有什么描述地不太妥当的地方欢迎提出。:)
      回复  引用  查看    
  8. #8楼  水煮 鱼      2008-02-24 23:16
    @Jeffrey Zhao
    相互学习
    呵呵,其实我不是做应用软件的,更和.NET更是边都沾不上
    不过我任务做技术都是相通的,何况还都是写程序。
    由于我们的产品是多CPU系统,而最近做的项目主要是涉及到他们之间的通信问题
    多CPU之间通信也存在着同步通信和异步通信的差别,和你文中提到的同步操作和异步操作其实一致,所以还是挺有同感。
      回复  引用  查看    
  9. #9楼 [楼主] Jeffrey Zhao      2008-02-24 23:23
    @水煮 鱼
    没错,做技术都是相通的。:)
    很多理论或方式方法无论在哪一层面究其本质都是一样的,比如同步异步,比如分层,比如缓存。
      回复  引用  查看    
  10. #10楼  毁于随      2008-02-25 09:02
    都从哪里学到的这些知识呢?
      回复  引用  查看    
  11. #11楼 [楼主] Jeffrey Zhao      2008-02-25 09:52
    @毁于随
    忘了,以前肯定在哪里看到过,呵呵。
      回复  引用  查看    
  12. #12楼  刘荣华! [未注册用户]2008-02-25 13:26
    异步编程是对传统的使用线程进行多线程任务的程序的优化和包装吧。
    因为线程说实在话,个人觉得比较难控制,尤其涉及到线程间的通信问题。虽然.NET很大程度上已经为这些问题提供了较方便的解决方案。

    另外对于IAsyncResult这个接口的用法,真的感觉挺别扭。还不如自己写事件和委托对线程里的程序状态进行控制和捕获。

    @毁于随
    PS:Windows核心编程里有对线程的比较详细和深入的介绍,另外操作系统方面的书籍应该都有。
      回复  引用    
  13. #13楼 [楼主] Jeffrey Zhao      2008-02-25 14:35
    @刘荣华!
    我倒觉得IAsyncResult比较好用。
    还有现在微软推出.NET并行库了,很好很强大。
      回复  引用  查看    
  14. #14楼  Indigo Dai      2008-02-25 21:31
    博客园需要的是这样的文章,而不是一些介绍性的文章,因为介绍性的东西大家都会都能做到,只是接触时间先后的问题,比如说VS 2008、ASP.NET 3.5体验一、二、三之类的。

    异步的确可以提高用户体验。

    微软的强大技术的确很强大也很易用,但是正是这种易用性宠坏了开发人员,使得面世的都是些低劣的应用,得到了易用性,大多数人是基本不深究下去,达到更巧妙的应用。老赵却不是这样的人,赫赫。

    另外关于ASP.NET中的异步调用,MSDN spotlight上也有一视频:
    Building Highly Scalable ASP.NET Web Sites by Exploiting Asynchronous Programming Models - Stefan Schackow -

    http://www.microsoft.com/emea/msdn/spotlight/result_search.aspx?speaker=141&product=0&rating=0&langue=0&x=63&y=17
    貌似这个不是传统的新技术宣传视频的典范。
      回复  引用  查看    
  15. #15楼  fox23      2008-02-25 23:13
    @水煮 鱼
    有一本LINUX内核编程讲异步I/O很清楚
    老赵说的是CLR层面之上的.
      回复  引用  查看    
  16. #16楼  小瑞克      2008-02-26 17:41
    学习了,期待下一篇
      回复  引用  查看    
  17. #17楼  jackyspy [未注册用户]2008-03-04 10:06
    谢谢好文章:)
      回复  引用    
  18. #18楼  yjmyzz@126.com [未注册用户]2008-03-11 14:12
    SqlCommand command;
    IAsyncResult ar = command.BeginExecuteNonQuery();
    while (!result.IsCompleted)
    {
    System.Threading.Thread.Sleep(100);
    }

    int result = command.EndExecuteNonQuery(ar);

    文中所说的最后一段代码,改成这样应该就可以了吧?MSDN官方文档上好象是这样说的

      回复  引用    
  19. #19楼  yjmyzz@126.com [未注册用户]2008-03-11 15:19
    一个异步查询页面,关键代码如下:

    //通过回调来实现异步查询
    private void BeginShowData()
    {
    string sql = "Select Top 10 F_No,F_Name From T_Product ";
    _conn = new SqlConnection(ConfigurationManager.ConnectionStrings["connStr"].ToString());
    _conn.Open();
    SqlCommand cmd = new SqlCommand(sql, _conn);
    IAsyncResult rIsynResult = cmd.BeginExecuteReader(new AsyncCallback(EndShowData), cmd, CommandBehavior.CloseConnection);
    //System.Threading.Thread.Sleep(100);//为什么去掉这一行就显示不出数据?
    }

    //回调方法
    public void EndShowData(IAsyncResult IResult)
    {
    if (!IResult.IsCompleted)
    {
    IResult.AsyncWaitHandle.WaitOne();
    }
    else
    {
    SqlDataReader dr = (IResult.AsyncState as SqlCommand).EndExecuteReader(IResult);
    if (!dr.IsClosed)
    {
    this.Repeater1.DataSource = dr;
    this.Repeater1.DataBind();
    }
    dr.Close();
    }
    }


    问题:为何BeginShowData方法中的System.Threading.Thread.Sleep(100);这一行去掉就显示不出数据?查了半天MSDN文档也没得到想要的解释,只能到这里问赵老师了?
      回复  引用    
  20. #20楼 [楼主] Jeffrey Zhao      2008-03-11 16:12
    @yjmyzz@126.com
    这是个示例,实际中使用这样的做法是得不到效果的。
    应该按照下面这片文章的做法来使用:
    http://www.cnblogs.com/JeffreyZhao/archive/2008/03/01/async-query-with-linq-to-sql.html
      回复  引用  查看    
  21. #21楼  银河使者      2008-07-13 21:34
    SqlCommand command;

    IAsyncResult ar = command.BeginExecuteNonQuery();
    int result = command.EndExecuteNonQuery(ar);

    这段代码和没使用异步一样了,应该使用回调方式。
      回复  引用  查看    
  22. #22楼 [楼主] Jeffrey Zhao      2008-07-13 21:37
    @银河使者
    你没有看清我文章的内容?
      回复  引用  查看    
  23. #23楼  airwolf2026      2008-07-14 18:05

    刚刚看了@银河使者 的文章,并试验了下.那XXXBegininvoke方法创建的线程,在系统任务管理器里面是可以看到的(+1);而老赵的文章又说这边的线程和系统的线程有所区别,这就蒙了...你的这个文章忘记是第几次看了哈,可能俺比较笨
      回复  引用  查看    
  24. #24楼  Allen Zhang      2008-07-18 22:58
    @Jeffrey Zhao
    我还是没理解不了,如何解决yjmyzz@126.com 提出的问题
    能否解释一下为什么不行呀?
      回复  引用  查看    
  25. #25楼 [楼主] Jeffrey Zhao      2008-07-19 19:20
    @Allen Zhang
    异步页面需要特殊写法——网上查一下
      回复  引用  查看    
  26. #26楼  weidagang2046      2008-11-23 21:50
    餐馆的例子很贴切。
      回复  引用  查看    
  27. #27楼  读者 [未注册用户]2008-11-24 13:58
    不错!
      回复  引用    
  28. #28楼  用情      2008-12-13 10:14
    老赵,什么时候讲讲“基于事件的异步模式“ 和 IAsyncResult异步调用 和使用System.Threading 自己控制多线程的使用和区别吧,不胜感激
      回复  引用  查看    
  29. #29楼  初始小花      2008-12-14 10:22
    写了不少WINFORM多线程了,一直没接触ASP.NET异步,一直以为ASP.NET异步就是服务端的多线程呢,原来是这样,学习,学习~
      回复  引用  查看    
  30. #30楼  Sunia [未注册用户]2009-01-10 14:51
    2009 迟到的第一个人,学习了!
      回复  引用    
  31. #31楼  iceboundrock      2009-01-19 16:25
    --引用--------------------------------------------------
    yjmyzz@126.com: SqlCommand command;
    <br>IAsyncResult ar = command.BeginExecuteNonQuery();
    <br>while (!result.IsCompleted)
    <br> {
    <br> System.Threading.Thread.Sleep(100);
    <br> }
    <br>
    <br>int result = command.EndExecuteNonQuery(ar);
    <br>
    <br>文中所说的最后一段代码,改成这样应该就可以了吧?MSDN官方文档上好象是这样说的
    <br>
    <br>
    --------------------------------------------------------
    这么写除了增加CPU占用,没有任何好处,还不如同步调用呢。

    其实文章里写的挺清楚了,异步跟多线程并不是一回事,尤其是在IO相关的操作上。
      回复  引用  查看    
  32. #32楼  水言木      2009-02-15 19:12
    好文章就是好文章。要是产量大点就更好了:P
      回复  引用  查看    

发表回复





发表评论

姓名 [登录] [注册] 

主页

Email(仅博主可见) 

验证码 *  验证码看不清,换一张

内容(请不要发表任何与政治相关的内容)  

登录  使用高级评论   新用户注册   返回页首      


相关文章:


历史上的今天:
2007-02-24 图灵奖40年来首次授予女性