Asynchronous Programming in
C# and Visual Basic

C# Visual Basic 中的异步编程

Mads Torgersen, Microsoft

October 2010

Contents

Why asynchronous?为什么要异步

How bad is it? 情况有多糟

A new approach一个新的途径

Tasks任务

The Task-based asynchronous pattern基于Task的异步模式

Async methods and awaiting异步方法与等待

Event handlers and void returning async methods事件处理例程与无返回的异步方法

Asynchronous lambda expressions异步的lambda表达式

Contexts

Beyond asynchronous: easing into parallelism超越异步:简单步入并行

 


 

When your user interface is unresponsive or your server doesn’t scale, chances are you need your code to be more asynchronous. With today’s .NET Framework and language features, though, that is easier said than done

当你的UI不再响应或你的服务器不具伸缩性,有可能你需要让你的代码要更异步。但是用如今的.NET架构和语言特性是说起来容易做起来难。

The Microsoft Visual Studio Async CTP proposes a new language feature in C# and VB, and a new framework pattern to go with it, that will make asynchronous programming similar to – and about as straightforward as –synchronous programming.

         Microsoft Visual Studio Async CTP提出了一项针对VBC#的新语言特性,与之伴随的还有一个新的架构模式,那将会让异步编程就像那“直率”的同步编程。

This document describes the limitations of the current callback based programming model for asynchrony, and describes the sweeping new opportunities offered by the framework and language features proposed in the Async CTP. It is intended as an overview, and further details can be found in specifications and detailed documents also included in the Async CTP.

该文档描述现今为达到异步所使用的基于编程模型的回调的一些局限性,同时也描述ASYNC CTP中新架构和新语言特性所显现出来的新机会。这只是一个概述,更多细节可以从规范和包含在ASYNC CTP中的详细文档中找到。

Async CTP: APIs and language features are described herein as they are intended to be. For technical and scheduling reasons the Async CTP differs in certain ways, which are called out in the text.

Async CTP:这里所描述的API和语言特性为定型版本。

Why asynchronous? 为什么异步?

For decades programming with remote resources has presented a conundrum. As the level of abstraction in “local” programming has been steadily rising, there has been a push for transparency of remote operations – they should look just like local ones, so that a developer doesn’t need to grapple with conceptual overhead, architectural impedance mismatch and leaky abstractions.

         数十年与远程资源打交道的编程经历显现出一个迷。“本地”编程的抽象度在稳步上涨,远程操作的透明度却在急速上涨:远程操作需要看起来就像本地操作,以便开发者可以不必抓住概念的顶层,体系结构阻抗失配与漏洞抽象化。

The problem is that remote operations are different from local ones. They have orders of magnitude more latency even at the best of times, may fail in new ways or simply never come back, depend on a variety of external factors beyond the developer’s control or even perception, etc. So while they can be represented like “just method calls,” it is not desirable to do so because the developer is left without handles to manage the special conditions arising from their remoteness – managing cancellation and timeouts, preserving threading resources during blocking waits, predicting and handling threats to responsiveness, etc.

         问题在于,远程操作不同于本地操作。即使在最好的时候远程操作也有数量级的延迟,有很多情况会导致失败或是直接不返回,以及大量的超出开发者控制甚至是认识的外部因素等,

On .NET we have not ignored this challenge. In fact we have not just one but several patterns for how to do asynchronous programming; that is, dealing with I/O and similar high latency operations without blocking threads. Most often there is both a synchronous (i.e. blocking transparently) and an asynchronous (i.e. latency-explicit) way of doing things. The problem is that these current patterns are very disruptive to program structure, leading to exceedingly complex and error prone code or (more commonly) developers giving up and using the blocking approach, taking a responsiveness and performance hit instead.

         .NET中我们也无法忽视这个挑战。实际上,我们针对异步编程有不少模式;例如:不用中断线程来处理I/O和类似的高延迟操作。完成一件事情一般都是同步(明显地中断)与异步(显式的延迟)共存的。问题在于这些现有的模式严重破坏程序结构,导致极高的复杂度和代码的潜在错误或是(更初级的)开发者放弃使用中断方法,进而引发更强烈的反应和性能问题。

The goal should be to bring the asynchronous development experience as close to the synchronous paradigm as possible, without letting go of the ability to handle the asynchrony-specific situations. Asynchrony should be explicit and non-transparent, but in a very lightweight and non-disruptive manner. Composability, abstraction and control structures should all work as simply and intuitively as with synchronous code.

         目标是尽可能的把异步开发体验接近同步模式,而同时不用丢弃处理异步细节的能力。异步应该是显式和非透明的,除了在非常轻量级和不混乱的模式。组合性,透明度和控制结构应与同步代码一样简单直观。

This is the goal of the features of the Async CTP.

         这就是 Async CTP的特性的目标。

How bad is it?情况有多糟?

The problem is best understood in the common scenario of a UI that has just one thread to run all its user interface code on, but applies equally in, for example, server scenarios where thread resources may be a scaling bottleneck and having thousands of threads spend most of their time doing nothing is a bad strategy.

         问题在于大家对UI的最好的认识是所有的用户 接口代码都运行在一个线程上,这个问题能得到最好的理解,例如:服务器脚本中线程资源可能瓶颈,因为上千个线程占用他们的时间却什么都不做是一个很糟的策略。

A client app that doesn’t react to mouse events or update the display for user-recognizable periods of time is likely the result of code holding on to the single UI thread for far too long. Maybe it is waiting for network IO or maybe it is performing an intensive computation. Meanwhile, other events just can’t get processing time, and the user-perceived world grinds to a halt. What’s amore frustrating user experience than losing all contact with an app that is “busy” standing still, staring down a pipe for a response that may be seconds away?

         一个客户端应用程序不响应鼠标事件或是不在用户可接受的时间范围内更新显示界面,就像代码在单线程上处理结果太久。可能它在等待网络IO 也可能是正在执行密集的计算。与此同时,其他事件无法得到处理时间,于是用户感知世界逐渐慢了下来。没什么能更让用户难过的经历就是与一个“忙到不东的应用程序失去所有联系,难道只能叼个烟斗玩等待?

Easy to say, hard to fix. For years the recommended approach to these issues has been asynchrony: don’t wait for that response. Return as soon as you issue the request, letting other events take place in the meantime, but have the eventual response call you back when it arrives so that you can process the result as a separate event. This is a great approach: your UI thread is never blocked waiting, but is instead blazing through small, nimble events that easily interleave and never have to wait long for their turn.

         说的容易,做起来难。多年来,解决这些问题的建议方式就是异步:不等待响应。处理完请求再返回,同时让其他事件也有一席之地,只要当处理完成后最终的答复回来通知你,你就可以在分离的事件中处理结果。

The problem: Asynchronous code totally blows up your control flow. The call you back part needs a callback – a delegate describing what comes after. But what if you wanted to “wait” inside a whileloop? An if statement? A try block or using block? How do you then describe “what comes after”?

         问题:异步代码让你的控制流程变得混乱。回来通知你的部分需要回调—一个委托。但是如果你需要在一个while 循环中或IF语句中“等待”?

Look at this simple example: 看下这个简单的示例:

public int SumPageSizes(IList<Uri> uris) {
    int total = 0;
    foreach (var uri in uris) {
        statusText.Text = string.Format("Found {0} bytes ...", total);
        var data = new WebClient().DownloadData(uri);
        total += data.Length;
    }
    statusText.Text = string.Format("Found {0} bytes total", total);
    return total;
}
Public Function SumPageSizes(uris As IList(Of Uri)) As Integer
    Dim total As Integer = 0
    For Each uri In uris
        statusText.Text = String.Format("Found {0} bytes ...", total)
        Dim data = New WebClient().DownloadData(uri)
        total += data.Length
    Next
    statusText.Text = String.Format("Found {0} bytes total", total)
    Return total
End Function


The method downloads a number of URI’s, totaling their sizes and updating a status text along the way.

该方法下载一系列的URI,总计他们的大小并同时更新状态文本。

Clearly this method doesn’t belong on the UI thread because it may take a very long time to complete, while holding up the UI completely. Just as clearly it does belong on the UI thread because it repeatedly updates the UI.What to do?

         很明显,该方法不属于UI线程,因为它需要花很长时间来完成,如果属于UI线程则它会完全地挂起UI。但它又应该属于UI线程,因为她重复地更新UI。该怎么办?

We can put it on a background thread, making it repeatedly “post” back to the UI thread to do the UI updates. That seems wasteful in this case, since a thread will be occupied spending most of its time just waiting for downloads, but sometimes it is really the only thing you can do. In this case, however, WebClient offers an asynchronous version of DownloadData – DownloadDataAsync – which returns promptly, and then fires an event – DownloadDataCompleted – when it is done. This allows us to write an asynchronous version of our method that splits it up into little callbacks and runs the next one on the UI thread whenever the download initiated by the previous one completes. Here’s a first attempt:

         我们可以把它放到一个后台线程,让它重复地“投”回到UI线程以更新UI。此例中它看起来太浪费了,因为一个线程将会使用大量的它的时间来等待下载,但是有些时候这又是唯一可做的。这个情况下,不管怎样,WebClient 提供一个异步的 DownloadData版本 —— DownloadDataAsync;当它完成时立即返回并引发一个事件——DownloadDataCompleted。这就允许我们写该方法的异步版本,它分割成一个小回调然后一旦下载完成则在UI线程运行下一次。这是我们的初尝试:

public void SumPageSizesAsync(IList<Uri> uris) {
    SumPageSizesAsyncHelper(uris.GetEnumerator(), 0);
}

private void SumPageSizesAsyncHelper(IEnumerator<Uri> enumerator, int total) {
    if (enumerator.MoveNext()) {
        statusText.Text = string.Format("Found {0} bytes ...", total);
        var client = new WebClient();
        client.DownloadDataCompleted += (sender, e) => {
            SumPageSizesAsyncHelper(enumerator, total + e.Result.Length);
        };
        client.DownloadDataAsync(enumerator.Current);
    }
    else{
        statusText.Text = string.Format("Found {0} bytes total", total);
        enumerator.Dispose();
    }
}

Public Sub SumPageSizesAsync(uris As IList(Of Uri))
    SumPageSizesAsyncHelper(uris.GetEnumerator(), 0)
End Sub

Private Sub SumPageSizesAsyncHelper(
        enumerator As IEnumerator(Of Uri), total As Integer)
    If enumerator.MoveNext() Then
        statusText.Text = String.Format("Found {0} bytes ...", total)
        Dim client = New WebClient
()
        AddHandler client.DownloadDataCompleted,
            Sub(sender, e)
                SumPageSizesAsyncHelper(enumerator, total + e.Result.Length)
            End Sub

        client.DownloadDataAsync(enumerator.Current)
    Else
        statusText.Text = String.Format("Found {0} bytes total", total)
        enumerator.Dispose()
    End If
End Sub



Already this is bad. We have to break up the neat foreach loop and manually get anenumerator. Each call to the private helper method hooks up an event handler for the completion of its download, that will eventually call the private helper again for the next element – if any. The code looks recursive instead of iterative.Still you may squint and be able to discern the intent of this code. But we are not nearly done yet.

这还是很糟。我们不得不打乱整洁的foreach循环然后手动得到一个枚举器。每次调用私有帮助器方法都要挂钩一个事件处理程序到它的下载完成事件,如果有的话最后都要为下一个元素再次调用私有帮助器。代码看似是递归的而不是重复的。只要你自己查看才能辨别代码的趋势。还好我们还没完。

The original code returned the total as well as display it. Our new asynchronous version returns to its caller way before the total has even been computed. How do we get a result back to our caller? The answer is: our caller must provide a callback to us – which we can then invoke with the total when ready. The caller must in turn have its code restructured so that it consumes the total in a callback instead of as a return value.

         该代码返回总量并显式它。我们新的异步版本在总数被计算前就返回给它的调用者。如何带着结果返回给调用者?答案是:我们调用者需要提供一个回调给我们——当准备好时我们可以伴随总数调用。调用者必须一次重建它的代码一边可以在回调中容纳总数而不是作为一个返回值。

And what about exceptions? The original code said nothing about exceptions; they were just silently propagated to the caller. In the async case, though, exceptions will arise after we returned to the caller. We must extend the callback from the caller to also tell it about exceptions, and we have to explicitly propagate those, wherever they may arise.

         遇到异常怎么办?源代码中没提及任何关于异常;他们只是安静地通知调用者。在这个异步情况下,虽然,异常将会引发在我们返回给调用者。我们必须从调用方扩展这个回调,告诉它关于异常处理。然后我们需要明确地传播这些,无论将会引发什么异常。

Together, these requirements will further clutter the code:

组合这些需求,代码应该是这样:

public void SumPageSizesAsync(IList<Uri> uris, Action<int, Exception> callback) {
    SumPageSizesAsyncHelper(uris.GetEnumerator(), 0, callback);
}

private void SumPageSizesAsyncHelper(IEnumerator<Uri> enumerator, int total,
    Action<int, Exception> callback) {
    try{
        if (enumerator.MoveNext()) {
            statusText.Text = string.Format("Found {0} bytes ...", total);
            var client = new WebClient();
            client.DownloadDataCompleted += (sender, e) => {
                if (e.Error != null) 
                {
                    enumerator.Dispose();
callback(0, e.Error);
                }
                else SumPageSizesAsyncHelper(
                    enumerator, total + e.Result.Length, callback);
            };
            client.DownloadDataAsync(enumerator.Current);
        }
        else {
            statusText.Text = string.Format("Found {0} bytes total", total);
            enumerator.Dispose();
            callback(total, null);
        }
    }
    catch (Exception ex) {
        enumerator.Dispose();
        callback(0, ex);
    }
}
Public Sub SumPageSizesAsync(
        uris As IList(Of Uri),  callback As Action(Of IntegerException))
    SumPageSizesAsyncHelper(uris.GetEnumerator(), 0, callback)
End Sub
Private Sub SumPageSizesAsyncHelper(
        enumerator As IEnumerator(Of Uri),
        total As Integer,
        callback As Action(Of IntegerException))
    Try
        If enumerator.MoveNext() Then
            statusText.Text = String.Format("Found {0} bytes ...", total)
            Dim client = New WebClient()
            AddHandler client.DownloadDataCompleted,
                Sub(sender, e)
                    If e.Error IsNot Nothing Then
                        enumerator.Dispose()
                        callback(0, e.Error)
                    Else
                        SumPageSizesAsyncHelper(
                            enumerator, total + e.Result.Length, callback)
                    End If
                End Sub
            client.DownloadDataAsync(enumerator.Current)
        Else
            statusText.Text = String.Format("Found {0} bytes total", total)
            enumerator.Dispose()
            callback(total, Nothing)
        End If
    Catch ex As Exception
        enumerator.Dispose()
        callback(0, ex)
    End Try
End Sub


Is this code now correct? Did we expand the foreach statement correctly, and propagate all exceptions? In fact when you look at it can you tell what it does?

代码现在对了吗?我们是否正确展开foreach语句?是否传播所有异常?实际上,当你看它的时候你能否说出它可以做什么?

Unlikely. And this corresponds to a synchronous method with just one blocking call to replace with an asynchronous one (DownloadData), and one layer of control structure around it (the foreach loop). Imagine trying to compose more asynchronous calls, or having more complex control structure! And we haven’t even started on the callers of SumPageSizesAsync!

         不是真的吧?这只是加了个中断并用异步的DownloadData替换了同步的方法,且只有一层控制结构(foreach循环)。想象试图组合更多的异步调用,或是拥有更复杂的控制结构!我们甚至还没开始谈及SumPageSizesAsync的调用者呢!

The real problem here is that we can no longer describe the logical flow of our method using the control flow constructs of the language. Instead of describing the flow, our program becomes about describing the wiring up of the flow. “Where to go next” becomes a matter not of the execution of a loop or conditional or try-block, but of which callback you installed.

         这里的实际问题是我们 不再用语言的控制流程构造来描述我们方法的逻辑流程。而是描述流程,我们的程序变成了只是描述流程组合。“下一步到哪里”成为执行循环、条件、TRY块的非重要因素,重要的是你设置的回调。

Hence, making your code asynchronous with today’s tools is extremely ungrateful: After a lot of hard work the result is unappealing, hard to read and likely full of bugs.

         因此 ,用现有的工具让你的代码异步是非常恶心的。在一系列繁重的工作后结果却令人不满:阅读困难且看似满是BUG

A new approach 新途径

With the proposed new features, the asynchronous version of the code will instead look like this:

用被提议的新特性 ,异步版本的代码看起来将是这样:

Public async Task<int> SumPageSizesAsync(IList<Uri> uris) {
    int total = 0;
    foreach (var uri in uris){
        statusText.Text = string.Format("Found {0} bytes ...", total);
        var data = await new WebClient().DownloadDataAsync(uri);
        total += data.Length;
    }
    statusText.Text = string.Format("Found {0} bytes total", total);
    return total;
}
Public Async Function SumPageSizesAsync(uris As IList(Of Uri)) As Task(Of Integer)
    Dim total As Integer = 0
    For Each uri In uris
        statusText.Text = String.Format("Found {0} bytes ...", total)
        Dim data = Await New WebClient().DownloadDataAsync(uri)
        total += data.Length
    Next
    statusText.Text = String.Format("Found {0} bytes total", total)
    Return total
End Function


Notice first how similar it is to the synchronous code. The highlighted parts are added, the rest stays the same. The control flow is completely unaltered, and there are no callbacks in sight. That doesn’t mean that there are no callbacks, but the compiler takes care of creating and signing them up, as we shall see.

         注意到没?它与异步代码有多像。高亮部分被添加,其余部分保持不变。控制流程一点都某变,并且没看到任何回调。这并不意味着这里没回调,而是编译器处理了回调的创建和约定,就像我们将看到的。

The method is asynchronous by virtue of returning a Task<int> instead of an int. TheTask and Task<T>types are in the framework today, and are good at representing ongoing work. The caller of SumPageSizesAsync can later use the returned Task<int> to inquire whether the work is complete, wait for the int result synchronously or sign up callbacks to get it when it is ready. So instead of taking a callback as a parameter, an asynchronous method now returns a representation of the ongoing work.

         通过借助返回一个 Task<int> 使该方法成为异步的。现今的 架构中已包含Task Task<T>类型,他们擅长表示持续的工作。SumPageSizesAsync 的调用者可以延后使用 返回的Task<int>来查询是否工作 已经完成,当准备好时同步的等待int结果或是调用回调。但并不是作为一个参数调用回调,一个异步方法现在返回一个持续工作的表示。

The asynchronous method has no extra parameters. By convention and to distinguish it from its synchronous counterpart (if we keep that in our code) we append “Async” to the method name.

         该异步方法没有额外的参数。通过添加约定的关键字”Async”到方法名以区分它和同步版本的区别。

The method is also marked as async. This means that the method body is compiled specially, allowing parts of it to be turned into callbacks, and automatically creating theTask<int>that is returned.

         该方法也被标记为 async。这意味着方法体以特别的方式编译:允许它的部分被返回到回调中,并在返回时自动创建 Task<int>

Here’s how that works: Inside of the method body we call another asynchronous method, DownloadDataAsync. This quickly returns a Task<byte[]> that will eventually complete when the downloaded data is available. However, we don’t want to do anything else until we have that data, so we immediately await the task, something that is only allowed inside of async methods.

         这是它的运作方式:在方法体中我们调用另一个异步方法,DownloadDataAsync。当下载的数据可用时,它快速返回一个 Task<byte[]>并最终完成。然而,在得到数据前我们不想做任何其他事情,所以我们立即 await (等待)任务,这是 async 方法中唯一允许的。

Async CTP:The Task-based version of DownloadDatais called DownloadDataTaskAsync to avoid conflict with an existing method on WebClient.

基于任务的DownloadDatais称为DownloadDataTaskAsync 以避免和WebClient中的方法冲突。


At first glance the await keyword looks like it blocks the thread until the task is complete and the data is available, but it doesn’t. Instead it signs up the rest of the method as a callback on the task, and immediately returns. When the awaited task eventually completes, it will invoke that callback and thus resume the execution of the method right where it left off!

         初看 await关键字像是它中断了线程直到任务完成和数据可用,其实不是。而是它把方法中的其余部分包装成一个回调放到任务上,并立即返回。当等待的任务完成,它会调用该回调并离开以恢复方法的执行。

By the time execution reaches the return statement, we have already been suspended and resumed several times by await’ing in the foreach loop, and have returned to the original caller long ago. We returned them not a result, because we didn’t have one yet (we were still in the process of computing the total) but a Task<int> that they could await if and when they wanted to. The effect of the return statement is to complete that task, so that whoever is looking at it – e.g. by await’ing it – can now get the result.

         届时,语句执行到了方法的返回,我们已在foreach循环中通过等待让方法挂起和恢复了数次,实际上很久前方法就返回给调用者了。但是并不是作为一个结果返回,因为我们还没结果(我们还仍然在计算总数),但是 Task<int>会在有需要 或是他们想要等待的时候等嗲。返回语句的效果是趋向完成任务,所以需要等待它才能得到结果。

In the following we will go into details with the different components of this approach.

         一下部分我们开始讲述新途径与不同组件中的细节。

Tasks

The Task and Task<TResult> types are already in the .NET Framework 4. A Task represents an ongoing activity, which may be CPU intensive work running on a separate thread, but may also represent an I/O operation, for example an outstanding response for an internet request.Task represents an activity without a result, and Task<TResult>, which derives from Task, represents an activity with a result of type TResult.

Task TASK<TRESULT>类型已经存在于.NET FRAMEWORK 4.0. TASK 表示一个持续性活动,该活动可能是CPU敏感的工作并运行在分离的线程,但也有可能表示一个IO操作,如一个针对internet请求的耗时应答。Task表示一个无结果的活动,Task<TResult> ,派生自Task,表示一个以TResult为结果的活动。

 

Tasks are often used to represent CPU-bound work running on a separate thread. Saying

Tasks 常用于表示CPU负载量很大的工作,并运行在分离的线程,也就是说:

 

Task<double> task = Task.Run(() =>LongRunningComputation(x, y, z));
Dim task As Task(Of Double) = Task.Run(Function() LongRunningComputation(x, y, z))


is an easy way to schedule the work in the lambda on a thread pool thread, immediately returning a task that will complete when the work is done.

Async CTP: Since the CTP installs on top of .NET 4, it is not possible for the CTP to add new members to existing types. While instance members are mimicked by extension methods, there is no way to add static members such as Task.Run. Instead these are offered on a “temporary” type called TaskEx.

Async CTP: 尽管 CTP 安装在.NET 4的顶端,但是它不可能添加一些新成员到已经存在的类型。尽管实例成员可以被扩展方法模拟,但是也无法添加静态成员如

Task.run .取而代之的是提供一些被称为TaskEx的“零时”类型

 

Manually creating a Task that does not run on a separate thread is also easy:

手动创建一个不运行在分离线程的 TASK也很简单:

var tcs = newTaskCompletionSource<double>();
return tcs.Task;

tcs.TrySetResult(result);
Dim tcs = NewTaskCompletionSource(Of Double)()
Return tcs.Task

tcs.TrySetResult(result)


Once you have a TaskCompletionSource you can immediately hand out its associated Task, and complete it later when the work is done and the result is ready.The tasks generated by the async language feature are of the latter sort – they don’t occupy a thread of their own.

只要你有一个TaskCompletionSource,你就可以立即提交他被分配的task,然后在工作完成时和结果准备好后完成它。任务组由异步语言特性生成,他们并不产生自己的线程。

Tasks can represent exceptional as well as successful completion. If a Task ends up in a faulted state, the relevant Exception will be available, and awaiting the Task will result in its exception being propagated.

任务也可以在未成功完成后表示一个异常。如果一个Task以错误状态结束,相关的异常将会可用,并等待该任务直到该异常被引发。

The easiest way to consume a Task is to await it in an async method, but there are also plenty of more “manual” ways to consume it – synchronously or asynchronously. These are outside the scope of this document (you can learn more about tasks in the MSDN documentation at http://msdn.microsoft.com/en-us/library/dd537609.aspx).

最简单的恢复的方法是在一个异步方法中等待它,同时也有很多“手动”的方式来恢复它-同步的或异步的。具体方法已超出本文范围(查看详细信息请参阅MSDN文档中的任务http://msdn.microsoft.com/en-us/library/dd537609.aspx).

posted on 2010-12-26 12:48  cyclone_dll  阅读(1453)  评论(0编辑  收藏  举报