async / await与手工制作的延续:是否巧妙地使用了ExecuteSynchronously?

我最近写了以下代码:

Task ExecAsync( string connectionString, SqlCommand cmd, Func resultBuilder, CancellationToken cancellationToken = default(CancellationToken) ) { var tcs = new TaskCompletionSource(); SqlConnectionProvider p; try { p = GetProvider( connectionString ); Task openTask = p.AcquireConnectionAsync( cmd, cancellationToken ); openTask .ContinueWith( open => { if( open.IsFaulted ) tcs.SetException( open.Exception.InnerExceptions ); else if( open.IsCanceled ) tcs.SetCanceled(); else { var execTask = cmd.ExecuteNonQueryAsync( cancellationToken ); execTask.ContinueWith( exec => { if( exec.IsFaulted ) tcs.SetException( exec.Exception.InnerExceptions ); else if( exec.IsCanceled ) tcs.SetCanceled(); else { try { tcs.SetResult( resultBuilder( cmd ) ); } catch( Exception exc ) { tcs.TrySetException( exc ); } } }, TaskContinuationOptions.ExecuteSynchronously ); } } ) .ContinueWith( _ => { if( !openTask.IsFaulted ) openTask.Result.Dispose(); }, TaskContinuationOptions.ExecuteSynchronously ); } catch( Exception ex ) { tcs.SetException( ex ); } return tcs.Task; } 

这按预期工作。 用async / await编写的相同代码(显然)更简单:

 async Task ExecAsync( string connectionString, SqlCommand cmd, Func resultBuilder, CancellationToken cancellationToken = default(CancellationToken) ) { SqlConnectionProvider p = GetProvider( connectionString ); using( IDisposable openTask = await p.AcquireConnectionAsync( cmd, cancellationToken ) ) { await cmd.ExecuteNonQueryAsync( cancellationToken ); return resultBuilder( cmd ); } } 

我快速浏览了两个版本生成的IL:async / await更大(不是一个惊喜)但我想知道async / await代码生成器是否分析了一个延续实际上是同步的事实使用TaskContinuationOptions.ExecuteSynchronously它可以…而我在IL生成的代码中找不到这个。

如果有人知道这个或有任何线索,我很高兴知道!

我想知道async / await代码生成器是否分析了一个事实,即继续实际上是同步使用TaskContinuationOptions.ExecuteSynchronously它可以…而我在IL生成的代码中找不到这个。

是否await继续 – 没有ConfigureAwait(continueOnCapturedContext: false – 异步或同步执行取决于在执行代码到达await点时执行代码的线程上是否存在同步上下文。 如果SynchronizationContext.Current != null ,则进一步的行为取决于SynchronizationContext.Post的实现。

例如,如果您在WPF / WinForms应用程序的主UI线程上,您的延续将在同一个线程上执行,但仍然是异步的 ,在将来的消息循环迭代中。 它将通过SynchronizationContext.Post发布。 这是因为先前的任务已经在线程池线程上完成,或者在不同的同步上下文中完成(例如, 为什么每个Dispatcher.BeginInvoke回调的唯一同步上下文? )。

如果先前任务已在具有相同同步上下文的线程上完成(例如,WinForm UI线程),则await继续将同步执行(内联)。 在这种情况下,不会使用SynchronizationContext.Post

在没有同步上下文的情况下,将在先前任务完成的同一线程上同步执行await继续。

这与您的ContinueWithTaskContinuationOptions.ExecuteSynchronously实现的不同之处在于,它完全不关心初始线程或完成线程的同步上下文,并且始终同步执行延续(尽管如此,此行为也有例外 ) 。

您可以使用ConfigureAwait(continueOnCapturedContext: false)来更接近所需的行为,但其语义仍然与TaskContinuationOptions.ExecuteSynchronously不同。 实际上,它指示调度程序不在具有任何同步上下文的线程上运行延续,因此您可能会遇到ConfigureAwait(false) 将延续推送到线程池的情况 ,而您可能期望同步执行。

还有关:重访Task.ConfigureAwait(continueOnCapturedContext: false)

此类优化在任务调度程序级别完成。 任务调度程序不只是有大量的任务要做; 它将它们分成每个工作线程的各种任务。 当从其中一个工作线程调度工作时(当你有很多延续时会发生很多事情),它会将它添加到该线程的队列中。 这确保了当您具有一系列延续的操作时,线程之间的上下文切换被最小化。 现在,如果线程用完了,它也可以从另一个线程的队列中提取工作项,这样每个人都可以保持忙碌状态。

当然,所有这些都说明,您在代码中等待的实际任务实际上都不是CPU限制的工作; 它们是IO绑定工作,因此它们不会在工作线程上运行,而这些线程可能会继续被重新用于处理延续,因为它们的工作首先不是由分配的线程完成的