PDC 2010:C#与Visual Basic的未来(中)
前几天在PDC 2010会议上Anders Hejlsberg发表了一场名为“The Future of C# and Visual Basic”的演说,谈论了未来C#和VB中最为重要的两个特性:“异步(Async)”及“编译器即服务(Compiler as a Service)”。我现在对这场演讲进行总结,但不会像上次《编程语言的发展趋势及未来方向》那样逐句翻译,而是以Anders的角度使用一种简捷合适的方式表述其完整内容。上一篇Anders讲述了async和await的使用方式,而这篇则是对这两个关键字的实现及效果作更进一步的解释。
异步方法的目标,是为了让代码与同步方法保持一致。微软要让代码充斥着回调函数,混乱不堪,它们完全不是逻辑上你想做的事情。可能您的代码中包含着一个核心模型,你也已经实现了,只是您现在想把它的执行过程变得异步化。您自己就可以享受到这一点。
与我们之前做的一些扩展一样,工作分为语言和框架两部分。语言的异步功能基于框架中的Task<T>,我们会围绕着Task<T>扩展框架,将它作为异步模型的核心。事实上,从Begin/End,或是基于事件的异步模型进行扩展往往只需要一两行封装的代码,于是您也可以得到自己的Task<T>模型。
而在语言方面,我们添加了两个新的关键字。一个是async关键字,用于把方法标记为异步。还有一个是await方法,用于等待异步工作完成,或者说是把控制权交换给调用方继续执行其他工作。这两个功能在C#和VB种均有体现。
那么什么是Task<T>呢?它表现的是一个“后续会继续进行的操作”,这可以是许多东西,Task<T>并不做任何限制,例如是一个异步I/O,后台工作线程等等,甚至可以是UI上的一个按钮,在用户点击之后任务就结束了。
Task<T>的优势在于,它使用一个对象封装了整个概念,您可以查询其结果或是状态,或是这个任务所引发的异常。您可以用它来构造一个可组合的异步模型,这正式我们目前的异步编程模型所不足的地方。
此外,它还提供了一个可组合的回调模型,您可以对一个任务指定说,在它结束之后执行另外一段代码,然后还可以对这个新的任务继续进行设定。这便构造出一个完整的逻辑流,框架会自行帮你完成这些工作。事实上await操作符便会自动把您的逻辑改写成这样的代码,它将您从Lambda表达式及回调函数中的逻辑里解放了出来,一切都交给编译器去做了。您可能会有些疑惑,不过其实这些都是编译器所擅长的事情。
由于我们统一了异步模型,我们就可以在此之上构建组合工具。例如WhenAll,它接受一系列的Task对象,并在全部结束之后返回所有结果。还有WhenAny,则等待第一个完成的任务,返回其结果。我们还有Delay,可以等待一段时间,但不占用任何资源。
沿着这个过程走一遍可能就会清晰一些。这里有个例子,一个异步方法调用另一个异步方法。我们假设这是在UI线程上执行的,消息会一个一个发送至UI线程上。
好,有人调用了DoWorkAsync,于是出现了一些任务。
DoWorkAsync的第一件事,是调用了ProcessFeedAsync。
ProcessFeedAsync方法是一个异步方法,所以它做的第一件事是构造一个表示任务的Task对象。
然后它调用了DownloadFeedAsync,这会创建另一个Task对象。然后,我们遇上了await操作符,这意味着ProcessFeedAsync后面的部分,将作为DownloadFeedAsync完成后的回调函数/continuation里的工作。
于是任务返回至DoWorkAsync,我们得到了t1这个对象。
同样的过程会再次出现,是为t2。
然后便调用了Task.WhenAll,这会创建一个新任务,表示前两个任务全部完成。于是这里的await操作符表示接下去的代码会在前两个任务完成后再继续下去。此时控制权便还给了DoWorkAsync的调用者,不会对线程造成负担。
在未来某一时刻t1和t2会执行完,我们假设t2先结束。此时它会说:我完成了,执行回调函数/continuation吧。
于是它会和发起线程的SynchronizationContext交互,给UI线程发一个消息,让后续任务在UI线程上继续执行──您的代码不用关注这些。现在代码运行至SaveDocAsync上了,这是另外一个异步任务。await让代码在这里返回,线程又可以执行目前还未结束的任务了。
于是SaveDocAsync任务完成了,UI线程又获得了一个消息执行后续工作。
此时任务便到达了ProcessFeedAsync的末尾,于是t2任务结束了。
继续等待,上面的过程会再次出现,最终t1也结束了。
当t1和t2完成以后,最后DoWorkAsync任务也终于结束了。可以看到,我们逻辑流程,无论是循环还是异常捕获都是同步的,但是其中的执行过程完全是异步的。
但是这又是如何实现的?我不会在这里说太细,这又是个完整的话题了。这里有一个例子,是一个异步方法,它会调用并await另一个异步方法。
而编译器则最终则生成类似于这样的代码。我只会提几点,首先,这是个状态机,编译器构造的其实就是个状态机,例如迭代器就是个状态机,事实上这里编译器的工作和yield之余迭代器的重写本质上没有太大区别。
其次就是关于任务的执行和等待,假如在等待时任务已经完成了,那么其实您是在同步地执行后续代码。我们没有必要交还控制,反正已经完成了,我们不妨就直接进行下去了。await有自己的模式,会决定这一任务是同步还是异步地执行。对于同步执行的任务,一切就继续执行下去了,直到某个需要异步执行的地方,便把控制权交还给调用方。
那么我们再来看一下异步之于Web服务的意义。这里有个ASP.NET页面,它会向数据库里获取许多RSS地址,然后下载到本地并解析:
private void ProcessData() { // ... var urls = new List<string>(); using (var conn = new SqlConnection(connectionString)) { conn.Open(); var cmd = new SqlCommand("GetUserFeeds", conn); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.AddWithValue("@UserID", user); using (var reader = cmd.ExecuteReader()) { while (reader.Read()) urls.Add(reader["FeedURL"].ToString()); } } var feeds = (from url in urls select CreateWebClient().DownloadString(url)).ToArray(); // ... }
这里用到了DownloadString这个同步下载数据的方法。执行下来大约要花费1秒多的时间。这里我不再演示令人痛苦的异步写法了,你必须在Page_Load和Page_PreRender各写一些逻辑,注册一些异步工作,或者就要启用一些后台线程,但这又会影响后台的线程池,对系统的表现会带来影响。
现在我来演示一些简单的异步化工作:
private async void ProcessData() { // ... var urls = new List<string>(); using (var conn = new SqlConnection(connectionString)) { conn.Open(); var cmd = new SqlCommand("GetUserFeeds", conn); cmd.CommandType = CommandType.StoredProcedure; cmd.Parameters.AddWithValue("@UserID", user); using (var reader = await cmd.ExecuteReaderAsync()) { while (reader.Read()) urls.Add(reader["FeedURL"].ToString()); } } var feeds = await TaskEx.WhenAll( from url in urls select CreateWebClient().DownloadStringTaskAsync(url)); // ... }
我们将DownloadString修改为DownloadStringTaskAsync,这样LINQ返回的就是一系列表示下载任务的Task对象,然后使用await及WhenAll等待它们全部完成。数据库查询也可以如此。这就是所有我们要做的事情。如今页面的执行效率有了很明显的提高。使用这个做法,我们可以很轻松地提高Web系统的伸缩能力。如今我们需要调用很多互相独立的服务的情况越来越多了,异步方法对此有很大帮助。
如今的异步场景有许多种,例如在后台执行一个计算任务,这是基于CPU的异步,还有基于网络或I/O的异步任务。这些都能用Task来表示出来,因为Task表示的就是未来会完成的异步任务。此外,有了async和.NET框架,我们则出现了另外一种任务,既基于某些任务组合而成的异步任务。这也就是async方法所体现出的异步任务,它可以让你使用传统的语句来构造异步执行过程。
例如有这么一个场景:获取链接,根据链接下载Youtube视频,根据下载到的视频创建mashup并组合起来。在执行这些工作的时候,我们也希望UI可以响应用户操作。
而要完成这些工作,代码可能只需要这么简单,完全就像同步代码一样。而这里也体现了多种异步任务:ScrapeYoutubeAsync是网络密集型任务,然后同时下载两个视频并等待它们结束。然后MashupVideosAsync是CPU密集型任务,然后最后则是I/O密集型的的SaveAsync操作。对于异常处理来说,我们可以简捷地使用一个try...catch,就像传统编程那样。
总结一下,一个异步方法可以让代码和同步实现一样简单,并统一了计算、网络及I/O的异步化。这可以用来创建高度伸缩的服务器程序,自然还有响应度高的UI程序。
在演讲的末尾,我会给出Visual Studio Async CTP的下载链接,我很乐于得到大家的反馈。
相关文章
- PDC 2010:C#与Visual Basic的未来(上)
- PDC 2010:C#与Visual Basic的未来(中)
- PDC 2010:C#与Visual Basic的未来(下)