C#5异步/等待 - 是*并发*?

时间:2011-10-05 14:40:56

标签: c# continuations async-await

我一直在考虑C#5中新的异步内容,并提出了一个特别的问题。

我理解await关键字是一个简洁的编译器技巧/语法糖来实现continuation passing,其中方法的其余部分被分解为Task个对象并排队到按顺序运行,但控制权返回给调用方法。

我的问题是我听说目前这一切都在一个线程上。这是否意味着这种异步内容实际上只是将延续代码转换为Task个对象,然后在每个任务完成之后调用Application.DoEvents()然后启动下一个任务?

或者我错过了什么? (问题的这一部分是修辞性的 - 我完全清楚我错过了某种东西 :))

5 个答案:

答案 0 :(得分:51)

它是并发,在某种意义上,许多未完成的异步操作可能随时都在进行中。它可能是也可能不是多线程

默认情况下,await会将延续计划回“当前执行上下文”。 “当前执行上下文”如果不是SynchronizationContext.Current则定义为null,如果没有TaskScheduler.Current则定义为SynchronizationContext

您可以通过调用ConfigureAwait并为false参数传递continueOnCapturedContext来覆盖此默认行为。在这种情况下,继续将不会被安排回该执行上下文。这通常意味着它将在线程池线程上运行。

除非您正在编写库代码,否则默认行为正是您所期望的。 WinForms,WPF和Silverlight(即所有UI框架)提供SynchronizationContext,因此继续在UI线程上执行(并且可以安全地访问UI对象)。 ASP.NET还提供SynchronizationContext,以确保继续在正确的请求上下文中执行。

其他线程(包括线程池线程,ThreadBackgroundWorker)不提供SynchronizationContext。因此,默认情况下,控制台应用和Win32服务根本没有SynchronizationContext。在这种情况下,continuation在线程池线程上执行。这就是为什么使用await / async的控制台应用程序演示包括对Console.ReadLine / ReadKey的调用或对Wait执行阻止Task的原因。

如果您发现自己需要SynchronizationContext,则可以使用AsyncContext库中的Nito.AsyncEx;它基本上只提供async兼容的“主循环”和SynchronizationContext。我发现它对Console应用程序和单元测试很有用(VS2012现在内置了对async Task单元测试的支持)。

有关SynchronizationContext的详情,请参阅my Feb MSDN article

在任何时候都不是DoEvents或等同的名字;相反,控制流一直返回,并且延续(函数的其余部分)计划在以后运行。这是一个更清洁的解决方案,因为它不会像使用DoEvents时那样导致重入问题。

答案 1 :(得分:5)

async / await背后的整个想法是它很好地执行continuation传递,并且不为该操作分配新线程。继续可能在新线程上发生,可能在同一个线程上继续。

答案 2 :(得分:2)

async / await的真正“meat”(异步)部分通常是单独完成的,与调用者的通信是通过TaskCompletionSource完成的。正如这里所写http://blogs.msdn.com/b/pfxteam/archive/2009/06/02/9685804.aspx

  

TaskCompletionSource类型有两个相关的目的,它们的名称都提到:它是创建任务的源,也是该任务完成的源。实质上,TaskCompletionSource充当Task及其完成的生产者。

并且示例非常清楚:

public static Task<T> RunAsync<T>(Func<T> function) 
{ 
    if (function == null) throw new ArgumentNullException(“function”); 
    var tcs = new TaskCompletionSource<T>(); 
    ThreadPool.QueueUserWorkItem(_ => 
    { 
        try 
        {  
            T result = function(); 
            tcs.SetResult(result);  
        } 
        catch(Exception exc) { tcs.SetException(exc); } 
    }); 
    return tcs.Task; 
}

通过TaskCompletionSource您可以访问可以等待的Task对象,但不是通过您创建多线程的async / await关键字。

请注意,当许多“慢”函数转换为async / await语法时,您不需要非常使用TaskCompletionSource。他们将在内部使用它(但最终某处必须有TaskCompletionSource才能产生异步结果)

答案 3 :(得分:1)

我想解释它的方式是“await”关键字只是等待一个任务完成,但在等待时产生执行到调用线程。然后,它返回任务的结果,并在任务完成后从“await”关键字后的语句继续。

我注意到的一些人似乎认为Task与调用线程在同一个线程中运行,这是不正确的,可以通过尝试在等待调用的方法中更改Windows.Forms GUI元素来证明。但是,继续在尽可能在调用线程中运行。

它只是一种简洁的方式,无需在任务完成时拥有回调委托或事件处理程序。

答案 4 :(得分:1)

我觉得这个问题需要为人们提供一个简单的答案。因此,我将简化一下。

事实是,如果您保存任务而不等待它们,则异步/等待是“并发的”。

var a = await LongTask1(x);
var b = await LongTask2(y);

var c = ShortTask(a, b);

不是并发的。 LongTask1将在LongTask2启动之前完成。

var a = LongTask1(x);
var b = LongTask2(y);

var c = ShortTask(await a, await b);

是并发的。

虽然我还敦促人们加深了解并对此进行阅读,但是您可以使用async / await进行并发,这很简单。