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

时间:2015-06-18 15:25:33

标签: c# asynchronous async-await continuations

我最近写了以下代码:

    Task<T> ExecAsync<T>( string connectionString, SqlCommand cmd, Func<SqlCommand, T> resultBuilder, CancellationToken cancellationToken = default(CancellationToken) )
    {
        var tcs = new TaskCompletionSource<T>();

        SqlConnectionProvider p;
        try
        {
            p = GetProvider( connectionString );
            Task<IDisposable> 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<T> ExecAsync<T>( string connectionString, SqlCommand cmd, Func<SqlCommand, T> resultBuilder, CancellationToken cancellationToken = default(CancellationToken) )
{
    SqlConnectionProvider p = GetProvider( connectionString );
    using( IDisposable openTask = await p.AcquireConnectionAsync( cmd, cancellationToken ) )
    {
        await cmd.ExecuteNonQueryAsync( cancellationToken );
        return resultBuilder( cmd );
    }
}

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

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

2 个答案:

答案 0 :(得分:10)

  

我想知道async / await代码生成器是否会分析这个事实   延续实际上是同步使用   TaskContinuationOptions.ExecuteSynchronously它可以......我   未能在IL生成的代码中找到它。

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

例如,如果您使用的是WPF / WinForms应用程序的主UI线程,则在将来迭代消息循环时,您的延续将在同一个线程上执行,但仍然是异步。它将通过SynchronizationContext.Post发布。这是在线程池线程或不同的同步上下文(例如,Why a unique synchronization context for each Dispatcher.BeginInvoke callback?)上完成了先行任务。

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

如果没有同步上下文,await延续将在先前任务完成的同一个线程上同步执行。

这与ContinueWith TaskContinuationOptions.ExecuteSynchronously实现不同,它完全不关心初始线程或完成线程的同步上下文,并且始终同步执行continuation(尽管如此,还有exceptions to this behavior

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

还相关:Revisiting Task.ConfigureAwait(continueOnCapturedContext: false).

答案 1 :(得分:0)

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

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