Difference Between Await and ContinueWith Keyword in C#

TPL is a new library introduced in C# version 4.0 to provide good control over threads, to allow use of multi-core CPUs using the parallel execution of threads. The following discussion is not about TPL but it's about the ContinueWith function available in the Task Class of the TPL and the await keyword introduced in C# 5.0 to support asynchronous calls.

ContinueWith

The ContinueWith function is a method available on the task that allows executing code after the task has finished execution. In simple words it allows continuation.

Things to note here is that ContinueWith also returns one Task. That means you can attach ContinueWith one task returned by this method.

Example

  1. public void ContinueWithOperation()  
  2. {  
  3.    Task<string> t = Task.Run(() => LongRunningOperation("Continuewith", 500));  
  4.    t.ContinueWith((t1) =>  
  5.    {  
  6.       Console.WriteLine(t1.Result);  
  7.    });  
  8. }   
In the preceding code a new task, LongRunningOperation, runs and once the task execution is completed the ContinueWith executes on the retuned task and prints the results of the task.

The ContinueWith operation is executed by the default thread scheduler. One can also provide an other scheduler for running the task on it, this is discussed later in this article.

Note: The following code is LongRunningOperation called by the task. LongRunningOpertion here is just an example in a real program. One cannot call a long-running operation on a task and if one wants to call longrunning task then one must pass TaskCreationOperation.LongRunning.

 

  1. private string LongRunningOperation(string s, int sec)  
  2. {  
  3.    Thread.Sleep(sec);  
  4.    return s + " Completed";  
  5. }  
await

The await keyword causes the runtime to run the operation on a new task and causes the executing thread to return and continue with an execution. (In most cases it executes the main thread of the application). Once the await operation finishes it returns to the statement where it left off (in other words it returns to the caller, in other words it returns depending on the state saved) and starts executing statements.

So await waits for a new task to finish and ensures the continuation once the execution of the waiting task is finished.

The await keyword is used with async to do asynchronous programming in C#. It's called asynchronous programming because the runtime captures the state of the program when it encounters the await keyword (that is similar to yield in an iterator) and restores the state back once the waited task finishes so the continuation runs in the correct context.

Example
  1. public async void AsyncOperation()  
  2. {  
  3.    string t = await Task.Run(() => LongRunningOperation("AsyncOperation", 1000));  
  4.    Console.WriteLine(t);  
  5. }  
In the preceding example the new task calls LongRunningOperation and the execution leaves once the main thread encounters the await keyword, in other words the main thread returns to the caller of AsyncOpetion and executes further. Once LongRunningOpertaion completes then the result of the task is printed on the console.

So here because the state is saved when await encountered flow returns on the same context one operation on the task is executed.

Note: State has the detail about executioncontext/synchronizationcontext.

So from the preceding it's clear that both Task.ContinueWith and await Task wait for the task to finish and allows continuation after the task completion. But they work differently.

Difference between ContinueWith and await: 
  1. Saving Sate for and return of the execution context

    ContinueWith doesn't save any kind of state, the continuation operation is attached using ContinueWith run on the default thread scheduler in case a scheduler is not provided.

    await: when encountering this keyword the state is saved and once the task on which await is done completes its execution the flow picks up the saved state data and starts the execution statement after await. (Note: State is having detail about executioncontext/synchronizationcontext.).

  2. Posting Completed Task result on UI Control

    The following is an example with ContinueWith and await to display the completion task result on the UI control.
    ContinueWith:

    Consider the following code that displays the result of completed task on the UI label.
    1. public void ContinueWithOperation()   
    2. {  
    3.     CancellationTokenSource source = new CancellationTokenSource();  
    4.     source.CancelAfter(TimeSpan.FromSeconds(1));  
    5.     Task < string > t = Task.Run(() = > LongRunningOperation("Continuewith", 500));  
    6.     t.ContinueWith((t1) = >   
    7.     {  
    8.         if (t1.IsCompleted && !t1.IsFaulted && !t1.IsCanceled) UpdateUI(t1.Result);  
    9.     });  
    10. }  
    11. private void UpdateUI(string s)   
    12. {  
    13.     label1.Text = s;  
    14. }  
    Note: LogRunningOperation is already given above.

    When the preceding code is executed the following runtime exception occurs.

    exception

    This exception occurs because the Continuation operation, the UpdateUI operation, runs on a different thread. In that case a thread will be provided by the default threadschedular ThreadPool and it doesn't have any information about the Synchronization context on which to run.

    To avoid an exception, one must pass the thread scheduler that passes data on the UI SynchronizationContenxt. In the following code the TaskScheduler.FromCurrentSynchronizationContext() method passes the UI-related thread scheduler.

    1. t.ContinueWith((t1) = >   
    2. {  
    3.     if (t1.IsCompleted && !t1.IsFaulted && !t1.IsCanceled) UpdateUI(t1.Result);  
    4. }, TaskScheduler.FromCurrentSynchronizationContext());  
    5. await: Consider below code which display result of completion on UI.  
    6. public async void AsyncOperation()   
    7. {  
    8.     try   
    9.     {  
    10.         string t = await Task.Run(() = > LongRunningOperation("AsyncOperation",  
    11.         10000));  
    12.         UpdateUI(t);  
    13.     }   
    14.     catch (Exception ex)   
    15.     {  
    16.         MessageBox.Show(ex.Message);  
    17.     }  
    18. }  
    Code written with await doesn't throw any exception when posting data to UI control, no special code is required for await. This happens because, as discussed in 1 point of difference, on encountering await state the data is saved that has the information about SynchronizationContext.

    So if one must post data on the UI then await is a good option because there is no need for extra care/code to post data on the UI.

     

  3. Handling Exception and Cancellation

    Consider the following example code for the ContinueWith and await methods for handing exceptions and a cancelled task.

    ContinueWith

    The following is sample code of how the exception/cancellation is handled using ContinueWith.
    1. public void ContinueWithOperationCancellation()   
    2. {  
    3.     CancellationTokenSource source = new CancellationTokenSource();  
    4.     source.Cancel();  
    5.     Task < string > t = Task.Run(() = > LongRunningOperationCancellation("Continuewith", 1500,  
    6.     source.Token), source.Token);  
    7.     t.ContinueWith((t1) = >   
    8.     {  
    9.         if (t1.Status == TaskStatus.RanToCompletion) Console.WriteLine(t1.Result);  
    10.         else if (t1.IsCanceled) Console.WriteLine("Task cancelled");  
    11.         else if (t.IsFaulted)   
    12.         {  
    13.             Console.WriteLine("Error: " + t.Exception.Message);  
    14.         }  
    15.     });  
    16. }  
    In the preceding code the cancellation/exception in the continuation is handled using TaskStatus. Another way to do the same thing is by using TaskContinuationOptions.OnlyOnRanToCompletion as in the following:
    1. t.ContinueWith(   
    2. (antecedent) => { },  
    3. TaskContinuationOptions.OnlyOnRanToCompletion);  
    await

    The following is sample code of how the exception/cancellation is handled using await.
    1. public async void AsyncOperationCancellation()   
    2. {  
    3.     try   
    4.     {  
    5.         CancellationTokenSource source = new CancellationTokenSource();  
    6.         source.Cancel();  
    7.         string t = await Task.Run(() = > LongRunningOperationCancellation("AsyncOperation", 2000, source.Token),  
    8.         source.Token);  
    9.         Console.WriteLine(t);  
    10.     }   
    11.     catch (TaskCanceledException ex)   
    12.     {  
    13.         Console.WriteLine(ex.Message);  
    14.     }   
    15.     catch (Exception ex)   
    16.     {  
    17.         Console.WriteLine(ex.Message);  
    18.     }  
    19. }  
    The preceding code doesn't use task status as continuewith, it uses a try ..catch block to handle the exception. To handle the cancellation there is a need for a catch block with TaskCanceledException.

    So from the preceding example my view is cancellation/exception handling is done in a very clean way when one uses continuation.

    The following is code for the LongRunningOperationCancellation method.
    1. private string LongRunningOperationCancellation(string s, int sec,   
    2. CancellationToken ct)  
    3. {  
    4.    ct.ThrowIfCancellationRequested();  
    5.    Thread.Sleep(sec);  
    6.    return s + " Completed";  
    7. }  
    The preceding three cases shows the difference of coding between ContinueWith and await. But the following one shows why await is better than ContinueWith.

  4. Complex flow

    Consider the following function for the calculation a factorial of a number.
    1. public KeyValuePair<intstring> Factorial(int i)  
    2. {  
    3.     KeyValuePair<intstring> kv;  
    4.     int fact = 1;  
    5.     for (int j = 1; j <= i; j++)  
    6.         fact *= j;  
    7.     string s = "factorial no " + i.ToString() + ":" + fact.ToString();  
    8.     kv = new KeyValuePair<intstring>(i, s);  
    9.     return kv;  
    10. }  
    The preceding function calculates the factorial of a number and returns a KeyValuePair to the caller to display the calculation.

    Now the problem statement is the preceding function to calculate the factorial of a number from 1 to 5.

    ContinueWith

    The following is code to calculate the factorial of the numbers between 1 and 5. The expectation from the code below is to calculate the factorial of a number then display it in order and once that is done a “Done” message is printed on the console.
    1. public void ContinueWithFactorial()   
    2. {  
    3.     for (int i = 1; i < 6; i++)   
    4.     {  
    5.         int temp = i;  
    6.         Task < KeyValuePair < intstring >> t = Task.Run(() = > Factorial(temp));  
    7.         t.ContinueWith((t1) = >   
    8.         {  
    9.             KeyValuePair < intstring > kv = t1.Result;  
    10.             Console.WriteLine(kv.Value);  
    11.         });  
    12.     }  
    13.     Console.WriteLine("Done");  
    14. }  
    Once the code is executed one will find that a “Done” message is printed immediately, in other words first then the factorial of a number is displayed on the screen. One more problem is the number factorial is not displayed in order. The following is the output of the code execution.

    output

    So to resolve the problem with the preceding code, the code must be refactored like this:
    1. public void FactContinueWithSeq(int i)   
    2. {  
    3.     Task < KeyValuePair < intstring >> t = Task.Run(() = > Factorial(i));  
    4.     var ct = t.ContinueWith(((t1) = >   
    5.     {  
    6.         KeyValuePair < intstring > kv = t1.Result;  
    7.         int seed = kv.Key;  
    8.         if (seed < 6)   
    9.         {  
    10.             Console.WriteLine(kv.Value);  
    11.             seed++;  
    12.             FactContinueWithSeq(seed);  
    13.         }   
    14.         else   
    15.         {  
    16.             Console.WriteLine("Done");  
    17.             return;  
    18.         }  
    19.     }));  
    20. }  
    The preceding function will be called like this p.FactContinueWithSeq(1).

    In the preceding code, to maintain sequence, a task must be fired one by one. And for that once one task completes its execution Contuation on it using the ContinueWith method the function is called again. It's like doing recursive calling to the function.

    And to display the “Done” message at the end it is necessary to check for the seed to function. That checks the seed value whether it increases by 6 or not.

    display

    But now there is the need for attaching a continuation on the completion of FactContinueWithSeq. To satisfy this the following code must be done.
    1. TaskCompletionSource<string> tcs = new TaskCompletionSource<string>();  
    2.        public Task<string> FactAsyncTask { get { return tcs.Task; } }  
    3.        public void FactContinueWithSeqAsync(int i)  
    4.        {  
    5.            Task<KeyValuePair<intstring>> t = Task.Run(() => Factorial(i));  
    6.            var ct = t.ContinueWith(((t1) =>  
    7.            {  
    8.                KeyValuePair<intstring> kv = t1.Result;  
    9.                int seed = kv.Key;  
    10.                if (seed < 5)  
    11.                {  
    12.                    Console.WriteLine(kv.Value);  
    13.                    seed++;  
    14.                    FactContinueWithSeqAsync(seed);  
    15.                }  
    16.                else  
    17.                {  
    18.                    tcs.SetResult("Execution done");  
    19.                }  
    20.            }));  
    21.        }  
    Call to the preceding function.
    1. p.FactContinueWithSeqAsync(1);  
    2. Task<string> t = p.FactAsyncTask;  
    3. t.ContinueWith((t1)=> Console.WriteLine(t.Result));  
    In the preceding code TaskCompletionSource is used to satisfy the code of providing the continutation on the completion of the factorial calculation.

    So one must do a lot of code to provide the expected results, that is to calculate the factorial in sequence, waiting on the calculation and once the calculation is completed a “Done” message is printed on the console.

    await

    Now the same code using await can be done like this:
    1. public async void AwaitWithFactorial()  
    2. {  
    3.    for (int i = 1; i < 6; i++)  
    4.    {  
    5.        int temp = i;  
    6.        Task<KeyValuePair<intstring>> t = Task.Run(() => Factorial(temp));  
    7.        await t;  
    8.        Console.WriteLine(t.Result.Value);  
    9.       }  
    10.       Console.WriteLine("Done");  
    11.   
    12. }  
    The preceding code is simple and clean and there is no need to do an entire big refactoration that is required when doing the same thing with ContinueWith.

Summary

From the preceding difference/comparison of ContinueWith vs await it is clear that in many scenarios using something and await is very helpful.

But there is also the scenario where more complex things do not require proper error/cancellation handling and in that case continuewith is helpful. But that scenario is very rare.

It's always good to go with a simple, easy and clear solution. For this my suggestion is to always go with await rather than ContinueWith.