在即将到来的新的Windows Runtime中更根本地确定任何API都不会运行超过50ms的时间。需要更长时间的操作将会由'kick off this operation'API来代替,不等待运算结果就直接立刻返回。这样做是因为Microsoft希望Windows8 Metro程序能够在即时的触控UI上能够“快速并且流动”,因为触控操作上即使是微小的停顿相比于用鼠标或者键盘来操作都会变得更加明显。从UI的角度来说,这是一项很有帮助的设计方案。

但是从开发者的角度来说,它会使编程变得更加麻烦。当我们读取文件或者调用WCF服务时,我们通常希望能够影响到结果。如果能够保证读取文件或者WCF服务返回时结果肯定可获得的,我们由上而下地写出容易理解和推理的代码。

string url = ReadUrlFromFile(filename); 
string contentOfUrl = HttpGetFromUrl(url); 
MessageBox.Show(contentOfUrl); 

这样的API被叫做同步或者阻塞。同步API易于理解和使用,不过在你的程序内部当前线程没有反应时,API就无法控制你的代码去做其它任务,因为它还不能传递结果。

拥使用即时返回的'kick off' API的方式叫做异步或者无阻塞。使用异步API编程更加繁难,因为你不能即时将结果返回给变量来保证运行:

string url = BeginReadUrlFromFile(filename);  // Won't work -- file read hasn't completed when BeginRead returns 
string contentOfUrl = BeginHttpGetFromUrl(url);  // Ditto 
MessageBox.Show(contentOfUrl); 

 相反,你不得不回调需要使用返回结果的代码,直到它已经准备好了:

    BeginReadUrlFromFile(filename, url => { 
        BeginHttpGetFromUrl(url, contentOfUrl => { 
          MessageBox.Show(contentOfUrl); 
        }); 
      }); 

甚至于这样一个简单的例子都显得相当丑陋。实际上,异步代码中需要更多的运算,更复杂的回调,逻辑条件,early exits以及错误处理,所以会更加难看。.NET框架中真正的异步API更加丑陋,到处都是 IAsyncResult对象和成对的EndXxx方法调用。

然而,如果我们希望能程序在Windows Runtime中运行,这就是用户所希望我们做的。

原来的解决方案:使用F#

F#背后聪明的人们想出来一个两全其美的解决方案。F#有一个叫做异步工作流程的特色功能,它由很多块异步引进的代码块组成。在异步工作流程中,你可以通过使用一个很像同步的语法来调用异步方法:

    async { 
      let! url = BeginReadUrlFromFile filename 
      let! contentOfUrl = BeginHttpGetFromUrl url 
      MessageBox.Show(contentOfUrl) 
    } 

F#编译器自动将这易于阅读、理解的代码转变为可怕的回调式等价物,这样你就可以简单地使用异步调用的响应行为从上而下地编程。

新的解决方案:使用C# 5

C#背后也有同样聪明的人,所以新的C#中也实现了这项功能。Visual Studio 11 beta中包含的下一版本的C#进了两个新关键字——"async" 和 "await"

关键字"async"表明使用的是异步调用方法。这对于调用者来说,理解它非常重要,因为这意味着方法会在它结束前返回——方法能够在异步调用时中途放弃而直接返回给它的调用者。

关键字"await"表明我们希望保证自上而下的逻辑 异步调用 而不是手动编写回调函数。下面是他们完美结合在一起的例子:

    public async void ShowReferencedContent(string filename) { 
      string url = await BeginReadFromFile(filename); 
      string contentOfUrl = await BeginHttpGetFromUrl(url); 
      MessageBox.Show(contentOfUrl); 
    } 

这样比回调更方便读、写和检查,但他们的作用完全相同。(实际上,这确实比回调更智能些,因为编译器并不会因为厌烦而跳过错误状况或者弄错early exit逻辑又或者忽略线程错误。)

当我们调用方法时发生了什么?首先是调用BeginReadFromFile方法,它提供了文件名和编译器生成的回调。BeginReadFromFile迅速返回,但结果仍然不可得。所以结果仍然不能分配给URL变量——回调的一部分——然后方法退出,返回给调用者!调用函数重新运行,并且保持它的代码持续运行,尽管被调用方法还没有结束。

然后在晚点时候,文件系统完成了阅读操作。这意味着结果现在是可获得的,Runtime安排回调。这并不一定会立刻发生——具体的时间还依赖于同步的环境。回调函数运行着,捆将URL变量和文件操作的结果绑定,然后调用BeginHttpGetFromUrl。它也会立刻返回,也就是说,方法会再一次退出。

最后,HTTP操作完成,回调函数第二次运行。它将绑定Url变量的内容和显示结果的消息框如果(如果有的话)。

我会希望向调用者返回什么值?

Async methods can exit before they’ve finished. So if an async method wants to return a result, it has to recognise that it might return to the caller before that result is available. For this reason, an async method that returns a value has to have a return type of Task rather than a ‘proper’ value. A Task represents a chunk of work which will eventually deliver a value, so a caller can examine the returned Task to determine when the result becomes available. Here’s how an async method looks when returning a value:

异步方法能够在结束前退出,所以,如果一个异步方法希望返回一个结果就不得不确认它是否在得到结果前就返回给调用者。因此,一个返回值的异步方法不得不包含一个Task返回类型而不是一个“合适的”值。一个Task代表最终会传递值的很大一块工作,所以调用者也能坚持返回的Task来确定什么时候会得到结果。下面是一个返回值的异步方法的样子:

    public static async Task<string> GetReferencedContent(string filename) 
    { 
      string url = await BeginReadFromFile(filename); 
      string contentOfUrl = await BeginHttpGetFromUrl(url); 
      return contentOfUrl; 
    } 

注意:尽管返回类型是Task<string>,返回状态接收的是一条字符串。再一次,编译器来管理返回状态产生一个Task。

现在调用者能够直接调用GetReferencedContent方法或者等待字符串变为可得,或者手动让它等待,又或者使它提前结束——无论如何它都适合使用结果。

Async-friendly APIs

如果你习惯在.NET 4或者更早之前版本上使用异步编程,你会习惯成对地使用Begin和End方法,比如WebRequest.BeginGetResponse 和WebRequest.EndGetResponse。这在.NET4.5中依然存在,但它们不使用await关键字。(主要是因为BeginXxx方法需要在回调中使用确切的方法调用来得到结果,而且编译器并不依赖EndXxx命名规范).NET4.5提供了返回Task对象的新方法,所以你可以调用WebRequest.GetResponseAsync来代替WebRequest.BeginGetResponse方法。下面是一个.NET4.5中使用异步API的一个实例:

    private static async Task<string> GetContent(string url) 
    { 
      WebRequest wr = WebRequest.Create(url); 
      var response = await wr.GetResponseAsync(); 
      using (var stm = response.GetResponseStream()) 
      { 
        using (var reader = new StreamReader(stm)) 
        { 
          var content = await reader.ReadToEndAsync(); 
          return content; 
        } 
      } 
    } 

这和使用 WebRequest.GetResponse() 和 TextReader.ReadToEnd()的同步代码是如此相似,只需要在API名后加上Async并且在方法前加上"await"关键字就可以了,相信你很快就能掌握它。

原文链接:mindscapehq.com

 

字符串 回调函数 编译器 线程 编程 最后修改于 2012-03-31 23:33:11
上一篇