Click here to Skip to main content
15,887,477 members
Articles / Programming Languages / C#

Addressing a Simple Yet Common C# Async/Await Misconception

Rate me:
Please Sign up or sign in to vote.
4.72/5 (41 votes)
19 Feb 2018CPOL2 min read 70.7K   181   65   54
I regularly come across developers who hold the misconception that code in a method will continue to be executed, in parallel to code in an awaited method call. So I'm going to demonstrate the behaviour we should expect in this article.

Async/await has been part of C# since C# 5.0 yet many developers have yet to explore how it works under the covers, which I would recommend for any syntactic sugar in C#. I won’t be going into that level of detail now, nor will I explore the subtleties of IO and CPU bound operations.

The Common Misconception

That awaited code executes in parallel to the code that follows.

i.e. in the following code, LongOperation() is called and awaited, then while this is executing and before it has completed, the code ‘do other things’ will start being executed.

C++
void DemoAsyncAwait()
{
    WithAwaitAtCallAsync().Wait();
}

async Task WithAwaitAtCallAsync()
{
    await LongOperation();

    // do other things
}

This is not how it behaves.

In the above code, what actually happens is that the await operator causes WithAwaitAtCallAsync() to suspend at that line and returns control back to DemoAsyncAwait() until the awaited task, LongOperation(), is complete.

When LongOperation() completes, then ‘do other things’ will be executed.

And If We Don't Await When We Call?

Then you do get that behaviour some developers innocently expect from awaiting the call, where LongOperation() is left to complete in the background while continuing on with WithoutAwaitAtCallAsync() in parallel, 'doing other things':

C#
void DemoAsyncAwait()
{
    WithoutAwaitAtCallAsync().Wait(); 
} 

async Task WithoutAwaitAtCallAsync() 
{ 
    Task task = LongOperation(); 

    // doing other things 
   
    await task;

    // more things to do
}

However, if LongOperation() is not complete when we reach the awaited Task it returned, then it yields control back to DemoAsyncAwait(), as above. It does not continue to complete 'more things to do' - not until the awaited task is complete.

Complete Console Application Example

Some notes about this code:

  • Always use await over Task.Wait() to retrieve the result of a background task (outside of this demo) to avoid blocking. I've used Task.Wait() in my demonstrations to force blocking and prevent the two separate demo results overlapping in time (I know in hindsight, Winforms would have been a better demo).
  • I have intentionally not used Task.Run() as I don't want to confuse things with new threads. Let's just assume LongOperation() is IO-bound.
  • I used Task.Delay() to simulate the long operation. Thread.Sleep() would block the thread.
C#
private static void Main(string[] args)
{
    // Demo 1
    Console.WriteLine(" Demo 1: Awaiting call to long operation:");
    Task withAwaitAtCallTask = WithAwaitAtCallAsync();
    withAwaitAtCallTask.Wait();

    // Demo 2
    Console.WriteLine(" Demo 2: NOT awaiting call to long operation:");
    Task withoutAwaitAtCallTask = WithoutAwaitAtCallAsync();
    withoutAwaitAtCallTask.Wait();

    Console.ReadKey();
}

private static async Task WithAwaitAtCallAsync()
{   
    Console.WriteLine(" WithAwaitAtCallAsync() entered.");

    Console.WriteLine(" Awaiting when I call LongOperation().");
    await LongOperation();

    Console.WriteLine(" Pretending to do other work in WithAwaitAtCallAsync().");
}

private static async Task WithoutAwaitAtCallAsync()
{
    Console.WriteLine(" WithoutAwaitAtCallAsync() entered.");

    Console.WriteLine(" Call made to LongOperation() with NO await.");
    Task task = LongOperation();

    Console.WriteLine(" Do some other work in WithoutAwaitAtCallAsync() after calling LongOperation().");

    await task;
}

private static async Task LongOperation()
{
     Console.WriteLine(" LongOperation() entered.");

     Console.WriteLine(" Starting the long (3 second) process in LongOperation()...");
     await Task.Delay(4000);
     Console.WriteLine(" Completed the long (3 second) process in LongOperation()...");
}

This is what happens when the code is executed (with colouring):

GiF of example

Summary

If you use the await keyword when calling an async method (from inside an async method), execution of the calling method is suspended to avoid blocking the thread and control is passed (or yielded) back up the method chain. If, on its journey up the chain, it reaches a call that was not awaited, then code in that method is able to continue in parallel to the remaining processing in the chain of awaited methods until it runs out of work to do, and then needs to await the result, which is inside the Task object returned by LongOperation().

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
Ben is the Principal Developer at a gov.uk and .NET Foundation foundation member. He previously worked for over 9 years as a school teacher, teaching programming and Computer Science. He enjoys making complex topics accessible and practical for busy developers.


Comments and Discussions

 
QuestionWould you have the Complete Final Code? Pin
Member 1378694317-Jul-18 22:04
Member 1378694317-Jul-18 22:04 
QuestionI don't understand Pin
tbayart2-Apr-18 22:30
professionaltbayart2-Apr-18 22:30 
QuestionWPF Example of Async method usage with (custom) TaskScheduler Pin
Dirk Bahle27-Mar-18 21:08
Dirk Bahle27-Mar-18 21:08 
GeneralMy vote of 5 Pin
KGustafson20-Feb-18 9:01
professionalKGustafson20-Feb-18 9:01 
QuestionCould I Suggest Using a Single Thread Synchronization Context for the Demo? Pin
George Swan19-Feb-18 11:41
mveGeorge Swan19-Feb-18 11:41 

In case anyone is interested, here is the code to implement a single thread synchronization context that can be used for Console apps. It's largely based on this post

C#
//https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/
   using System;
   using System.Collections.Concurrent;
   using System.Threading;
   using System.Threading.Tasks;

   public sealed class SingleThreadSynchronizationContext : SynchronizationContext
   {
       #region Constants and Fields

       private readonly BlockingCollection<Action> messageQueue = new BlockingCollection<Action>();

       #endregion

       #region Public Methods and Operators

       public override void Post(SendOrPostCallback func, object state)
       {
           messageQueue.Add(() => func(state));
       }

       public void Run(Func<Task> func)

       {
           SynchronizationContext prevContex = Current;
           SetSynchronizationContext(this);
           try
           {
               //ContinuationTask runs on a threadpool thread
               Task ContinuationTask = func().ContinueWith(
                   t => StopMessagePump());
               StartMessagePump();
               //the foreground thread only runs this after the message pump is stopped
               ContinuationTask.Wait();
           }

           finally
           {
               SetSynchronizationContext(prevContex);
               messageQueue.Dispose();
           }
       }

       public void StartMessagePump()
       {
           Action message;
           while (messageQueue.TryTake(out message, Timeout.Infinite))
           {
               message();
           }
       }

       public void StopMessagePump()
       {
           //throws an OperationCanceledException if the BlockingCollection is empty
           //The exception is handled by the framework
           messageQueue.CompleteAdding();
       }

       #endregion
   }

Your demo can now be run like this.

C#
private static void Main(string[] args)
       {
           var singleThreadSyncContext = new SingleThreadSynchronizationContext();
           singleThreadSyncContext.Run(() => MainAsync());
       }

       private static async Task MainAsync()
       {
           // Demo 1
           Console.WriteLine(" Demo 1: Awaiting call to long operation:");
           await WithAwaitAtCallAsync();

           // Demo 2
           Console.WriteLine(" Demo 2: NOT awaiting call to long operation:");
           await WithoutAwaitAtCallAsync();
           Console.ReadKey();
       }


QuestionDisgusting article. Pin
Member 1171790117-Feb-18 0:58
Member 1171790117-Feb-18 0:58 
GeneralRe: Pin
Ben Hall (failingfast.io)17-Feb-18 5:49
professionalBen Hall (failingfast.io)17-Feb-18 5:49 
GeneralRe: Pin
CHill6020-Mar-18 6:30
mveCHill6020-Mar-18 6:30 
QuestionDon't forget to start the task... Pin
Bernhard Hiller16-Feb-18 2:45
Bernhard Hiller16-Feb-18 2:45 
AnswerRe: Don't forget to start the task... Pin
Ben Hall (failingfast.io)16-Feb-18 5:24
professionalBen Hall (failingfast.io)16-Feb-18 5:24 
AnswerRe: Don't forget to start the task... Pin
ipavlu23-Mar-18 8:29
ipavlu23-Mar-18 8:29 
PraiseReally helpful thank you Pin
Olly Carter14-Feb-18 23:22
Olly Carter14-Feb-18 23:22 
SuggestionWinform example? Pin
KLPounds14-Feb-18 11:46
KLPounds14-Feb-18 11:46 
AnswerRe: Winform example? Pin
Ben Hall (failingfast.io)19-Feb-18 9:46
professionalBen Hall (failingfast.io)19-Feb-18 9:46 
SuggestionDisappointed... Pin
mldisibio14-Feb-18 9:49
mldisibio14-Feb-18 9:49 
GeneralRe: Disappointed... Pin
Ben Hall (failingfast.io)14-Feb-18 23:15
professionalBen Hall (failingfast.io)14-Feb-18 23:15 
GeneralRe: Less Disappointed Now... :-) Pin
mldisibio15-Feb-18 5:30
mldisibio15-Feb-18 5:30 
QuestionStill confused Pin
Steve Grattan14-Feb-18 2:58
Steve Grattan14-Feb-18 2:58 
AnswerRe: Still confused Pin
Ben Hall (failingfast.io)14-Feb-18 5:58
professionalBen Hall (failingfast.io)14-Feb-18 5:58 
GeneralRe: Still confused Pin
Steve Grattan14-Feb-18 12:49
Steve Grattan14-Feb-18 12:49 
AnswerRe: Still confused Pin
tolsen6414-Feb-18 10:20
professionaltolsen6414-Feb-18 10:20 
GeneralRe: Still confused Pin
Steve Grattan14-Feb-18 12:45
Steve Grattan14-Feb-18 12:45 
GeneralRe: Still confused Pin
tolsen6414-Feb-18 12:59
professionaltolsen6414-Feb-18 12:59 
Questionchanging 1 line changes the outcome Pin
williams200014-Feb-18 2:35
professionalwilliams200014-Feb-18 2:35 
AnswerRe: changing 1 line changes the outcome Pin
Ben Hall (failingfast.io)14-Feb-18 5:43
professionalBen Hall (failingfast.io)14-Feb-18 5:43 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.