I recently wrote the following code:
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;
}
This works as intended. The same code written with async/await is (obviously) simpler:
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 );
}
}
I had a quick look at the generated IL for the 2 versions: the async/await is bigger (not a surprise) but I was wondering if the async/await code generator analyses the fact that a continuation is actually synchronous to use TaskContinuationOptions.ExecuteSynchronously
where it can... and I failed to find this in the IL generated code.
If anyone knows this or have any clue about it, I'd be pleased to know!
Whether
await
continuations - withoutConfigureAwait(continueOnCapturedContext: false
) - execute asynchronously or synchronously depends on the presence of a synchronization context on the thread which was executing your code when it hit theawait
point. IfSynchronizationContext.Current != null
, the further behavior depends on the implementation ofSynchronizationContext.Post
.E.g., if you are on the main UI thread of a WPF/WinForms app, your continuations will be executed on the same thread, but still asynchronously, upon some future iteration of the message loop. It will be posted via
SynchronizationContext.Post
. That's provided the antecedent task has completed on a thread pool thread, or on a different synchronization context (e.g., Why a unique synchronization context for each Dispatcher.BeginInvoke callback?).If the antecedent task has completed on a thread with the same synchronization context (e.g. a WinForm UI thread), the
await
continuation will be executed synchronously (inlined).SynchronizationContext.Post
will not be used in this case.In the absence of synchronization context, an
await
continuation will be executed synchronously on the same thread the antecedent task has completed on.This is how it is different from your
ContinueWith
withTaskContinuationOptions.ExecuteSynchronously
implementation, which doesn't care at all about the synchronization context of either initial thread or completion thread, and always executes the continuation synchronously (there are exceptions to this behavior, nonetheless).You can use
ConfigureAwait(continueOnCapturedContext: false)
to get closer to the desired behavior, but its semantic is still different fromTaskContinuationOptions.ExecuteSynchronously
. In fact, it instructs the scheduler to not run a continuation on a thread with any synchronization context, so you may experience situations whereConfigureAwait(false)
pushes the continuation to thread pool, while you might have been expecting a synchronous execution.Also related: Revisiting
Task.ConfigureAwait(continueOnCapturedContext: false)
.