59

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

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

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

还是我错过了什么?(这部分问题是修辞性的——我完全知道我遗漏了一些东西:))

4

5 回答 5

59

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

默认情况下,await会将继续安排回“当前执行上下文”。“当前执行上下文”被定义为SynchronizationContext.Current如果它是 non- null,或者TaskScheduler.Current如果没有SynchronizationContext

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

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

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

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

有关详细信息SynchronizationContext,请参阅我的 2 月 MSDN 文章

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

于 2011-10-05T15:25:49.307 回答
5

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

于 2011-10-05T14:46:14.450 回答
2

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

TaskCompletionSource 类型有两个相关的用途,两者都由其名称暗示:它是创建任务的源,以及完成该任务的源。本质上,TaskCompletionSource 充当任务及其完成的生产者。

这个例子很清楚:

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一个异步结果)

于 2011-10-05T14:49:22.793 回答
1

我喜欢解释它的方式是“await”关键字只是等待任务完成,但在等待时将执行交给调用线程。然后它返回任务的结果,并在任务完成后从“await”关键字之后的语句继续。

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

它只是在任务完成时不必有回调委托或事件处理程序的一种巧妙方法。

于 2011-11-03T09:52:39.747 回答
1

我觉得这个问题需要一个更简单的答案。所以我要过分简单化。

事实是,如果您保存任务并且不等待它们,那么 async/await 是“并发的”。

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 进行并发,这非常简单。

于 2020-02-26T21:37:43.793 回答