Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

async/await vs. hand made continuations: is ExecuteSynchronously cleverly used?

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!

like image 516
Spi Avatar asked Jun 18 '15 15:06

Spi


People also ask

Why do we use async and await in C#?

The async keyword turns a method into an async method, which allows you to use the await keyword in its body. When the await keyword is applied, it suspends the calling method and yields control back to its caller until the awaited task is complete. await can only be used inside an async method.

Is async property allowed in C#?

There is no technical reason that async properties are not allowed in C#. It was a purposeful design decision, because "asynchronous properties" is an oxymoron. Properties should return current values; they should not be kicking off background operations.

Does await Block C#?

When the await operator is applied to the operand that represents an already completed operation, it returns the result of the operation immediately without suspension of the enclosing method. The await operator doesn't block the thread that evaluates the async method.

Does async await create new thread?

The async and await keywords don't cause additional threads to be created. Async methods don't require multithreading because an async method doesn't run on its own thread. The method runs on the current synchronization context and uses time on the thread only when the method is active.


1 Answers

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.

Whether await continuations - without ConfigureAwait(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 the await point. If SynchronizationContext.Current != null, the further behavior depends on the implementation of SynchronizationContext.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 with TaskContinuationOptions.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 from TaskContinuationOptions.ExecuteSynchronously. In fact, it instructs the scheduler to not run a continuation on a thread with any synchronization context, so you may experience situations where ConfigureAwait(false) pushes the continuation to thread pool, while you might have been expecting a synchronous execution.

Also related: Revisiting Task.ConfigureAwait(continueOnCapturedContext: false).

like image 105
noseratio Avatar answered Sep 19 '22 06:09

noseratio