🚀Async/Await Deep Dive - Asynchronous Programming

Introduction

In this series, we will try to understand the benefits of Async/Await keyword while working on Asynchronous programming. 

I am going to cover this topic in two parts:

  1. Basic concepts and common terminology used
  2. Best practices for Async/Await to make your application more efficient

Basic Concept and common terminology used

Async/Await are contextual keywords, which are used by new generation apps to take advantage of Asynchronous Programming. Although these are wrappers on the task library which makes the code more readable and easier to maintain.

These keywords also increase the chances of increased EXCEPTION, which is possibly untracked, and potentially introduces a deadlock in an application if not used properly.

When we talk about Task/Threads, two terms are always used, Thread Pool and State Machine. What exactly are these?

Thread Pool

This is where all threads sit in a machine. Basically, two types of thread will be maintained by CLR in the Thread pool.

  • Worker thread:
    Just refers to any thread other than the main thread that does some 'work' on behalf of the application that spawned the thread. 'Work' could really mean anything, including waiting for some I/O to complete. The thread pool keeps a cache of worker threads because threads are expensive to create.
     
  • Asynchronous I/O thread:
    The term 'I/O thread' in .NET/CLR refers to the threads the Thread Pool reserves in order to dispatch Native Overlapped callbacks from "overlapped" win32 calls (also known as "completion port I/O"). The CLR maintains its own I/O completion port and can bind any handle to it (via the ThreadPool.BindHandle API).

State Machine

The state machine is where task will be executed and will reference which thread initiated that task so that when that task is completed, the state machine will notify the caller thread that the task has finished.

Async Keyword

Async indicate that this method will be executed asynchronously. It will run in State machine. When we add the "Async" keyword in method signature, the compiler will create a class based on method name inherited that from State Machine interface.

Await Keyword

Every Task should be awaited. If not, then executing the thread won't wait for that Task, which is executing asynchronously. Basically, it returns to caller thread with reference to ongoing task and stop execution of code below that line and release the current thread to thread pool to process another request.

Async and await are always used together, if not, then there will be something wrong.

Dead Locks

When the UI thread is waiting to complete some asynchronous task, and inside that Async method, we are trying to reflect something in UI control, then we have created a deadlock.

In a Web application, when we say Task is running in State machine, it means the state machine is running on a UI thread. If we use SomeAsyncMethod().Wait(), the UI thread is a block and the State machine now does not know the returning caller Thread and application will be in the deadlock stage.

Let’s see some code:

I have a WPF app that has one Btn click event and one label to display the text.

In the below code, are you able to identify the problem?

private void RunApp_Click(object sender, RoutedEventArgs e)
{
    var t1 = Task.Run(() =>
    {
        OutPutText.Text = "This text will be set from different thread.....!!!!!";
    });
}

Correct, we aren't able to see the text assigned to OutPutText(Label) control, so there will be no error thrown to the surface. If this is not working code, then we should get error. We will see where the error has gone in a moment. 

What is problem in above code?

The UI thread will invoke the Task and will run to completion without waiting for "t1" task to complete and when task "t1" which will go to write text in the label marked as completed and try to assign a value on a label control that it cannot.

Why was the exception not thrown if this is not working code?

The task is running on the State machine. If there is an exception on the task, it will shallow by Task itself because the whole code will be executed inside a try-catch block. The below code shows the compile version of “RunApp_Click". I highlighted some important pieces in yellow.

[CompilerGenerated]  
private sealed class <RunApp_Click>d__1 : IAsyncStateMachine  
{  
    public int <>1__state;  
    public AsyncVoidMethodBuilder <>t__builder;  
    public object sender;  
    public RoutedEventArgs e;  
    public MainWindow <>4__this;  
    private Task <t1>5__1;  
  
    private void MoveNext()  
    {  
        int num = this.<>1__state;  
        try  
        {  
            this.<t1>5__1 = Task.Run(new Action(this.<>4__this.<RunApp_Click>b__1_0));  
        }  
        catch (Exception exception)  
        {  
            this.<>1__state = -2;  
            this.<>t__builder.SetException(exception);  
            return;  
        }  
        this.<>1__state = -2;  
        this.<>t__builder.SetResult();  
    }  
  
    [DebuggerHidden]  
    private void SetStateMachine(IAsyncStateMachine stateMachine)  
    {  
    }  
}

How to track exception in this scenario?

Task lib has a method called "ContinueWith", which will execute when the Task marks itself as competed, either with a success or failure.

private async void RunApp_Click(object sender, RoutedEventArgs e)  
{  
    var t1 = Task.Run(() =>  
    {  
        OutPutText.Text = "This text will be set from different thread.....!!!!!";  
    });  
  
    t1.ContinueWith(t =>  
    {  
        if (t.IsFaulted)  
        { /*Handel the exception*/}  
    });  
}

Time to see the solution of above problem

Basically we have many ways to solve this problem. We will see some of the best ways.

Solution 1

Through DISPATCHER. Since this example is from WPF, we can use dispatcher to allow UI to change it.

private async void RunApp_Click(object sender, RoutedEventArgs e)  
{  
    var t1 = Task.Run(() =>  
    {  
        Dispatcher.Invoke(() =>  
        {  
            OutPutText.Text = "This text will be set from different thread.....!!!!!";  
        });  
    });   
}

Solution 2

By using TaskScheduler class, it has one "FromCurrentSynchronizationContext" method. This method creates a TaskScheduler associated with the current SynchronizationContext that lets the completed task know which thread initiated that one.

private async void RunApp_Click(object sender, RoutedEventArgs e)    
{    
    var t1 = Task.Run(() =>    
    {   
        //do some business logic    
        Task.Delay(2000);      
    }).ContinueWith(t =>    
    {      
        OutPutText.Text = "This text will be set from different thread.....!!!!!";   
    }, TaskScheduler.FromCurrentSynchronizationContext());    
}

Solution 3

How we can do this with Async/Await?

private async void RunApp_Click(object sender, RoutedEventArgs e)  
{  
    //Best and easy way to do  
    await RunAsync();
    OutPutText.Text = "This text will be set from different thread.....!!!!!";
}  
  
private async Task RunAsync()  
{  
    //do some business logic  
    await Task.Delay(2000);
}

As we can see, it's easy to write code that's maintainable. This hides all complexity behind the hood. Lets see how it will execute behind the screen.

When the UI thread sees await RunAsync(), it will execute RunAsync in State Machine and free the UI thread so that the UI will not freeze. As soon as RunAsync completes, the State machine will let the UI thread (caller thread) to complete the process. 

Let's see the comply code of when we used Async/Await.

First see the state machine compile code of RunApp_Click: 

[CompilerGenerated]  
private sealed class <RunApp_Click>d__1 : IAsyncStateMachine  
{  
    public int <>1__state;  
    public AsyncVoidMethodBuilder <>t__builder;  
    public object sender;  
    public RoutedEventArgs e;  
    public MainWindow <>4__this;  
    private TaskAwaiter <>u__1;  
  
    private void MoveNext()  
    {  
        int num = this.<>1__state;  
        try  
        {  
            TaskAwaiter awaiter;  
            if (num == 0)  
            {  
                awaiter = this.<>u__1;  
                this.<>u__1 = new TaskAwaiter();  
                this.<>1__state = num = -1;  
                goto TR_0004;  
            }  
            else  
            {  
                awaiter = this.<>4__this.RunAsync().GetAwaiter();   //Invoking RunAsync and waiting for to complete
                if (awaiter.IsCompleted)  
                {  
                    goto TR_0004;  
                }  
                else  
                {  
                    this.<>1__state = num = 0;  
                    this.<>u__1 = awaiter;  
                    MainWindow.<RunApp_Click>d__1 stateMachine = this;  
                    this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, MainWindow.<RunApp_Click>d__1>(ref awaiter, ref stateMachine);  
                }  
            }  
            return;  
        TR_0004:  
            awaiter.GetResult();  
            this.<>4__this.OutPutText.Text = "This text will be set from different thread.....!!!!!";    //After RunAsync will complete this line will of code will be executed
            this.<>1__state = -2;  
            this.<>t__builder.SetResult();  
        }  
        catch (Exception exception)  
        {  
            this.<>1__state = -2;  
            this.<>t__builder.SetException(exception);  
        }  
    }
}

Let's see the state machine compile code of RunAsync method.

[CompilerGenerated]  
private sealed class <RunAsync>d__2 : IAsyncStateMachine  
{  
    public int <>1__state;  
    public AsyncTaskMethodBuilder <>t__builder;  
    public MainWindow <>4__this;  
    private TaskAwaiter <>u__1;  
  
    private void MoveNext()  
    {  
        int num = this.<>1__state;  
        try  
        {  
            TaskAwaiter awaiter;  
            if (num == 0)  
            {  
                awaiter = this.<>u__1;  
                this.<>u__1 = new TaskAwaiter();  
                this.<>1__state = num = -1;  
                goto TR_0004;  
            }  
            else  
            {  
                awaiter = Task.Delay(0x7d0).GetAwaiter();  
                if (awaiter.IsCompleted)  
                {  
                    goto TR_0004;  
                }  
                else  
                {  
                    this.<>1__state = num = 0;  
                    this.<>u__1 = awaiter;  
                    MainWindow.<RunAsync>d__2 stateMachine = this;  
                    this.<>t__builder.AwaitUnsafeOnCompleted<TaskAwaiter, MainWindow.<RunAsync>d__2>(ref awaiter, ref stateMachine);  
                }  
            }  
            return;  
        TR_0004:  
            awaiter.GetResult();  
            this.<>1__state = -2;  
            this.<>t__builder.SetResult();  
        }  
        catch (Exception exception)  
        {  
            this.<>1__state = -2;  
            this.<>t__builder.SetException(exception);  
        }  
    }
}

Let us see the RunAsync Compile code.

[AsyncStateMachine(typeof(<RunAsync>d__2)), DebuggerStepThrough]  
private Task RunAsync()  
{  
    <RunAsync>d__2 stateMachine = new <RunAsync>d__2 {  
        <>4__this = this,  
        <>t__builder = AsyncTaskMethodBuilder.Create(),  
        <>1__state = -1  
    };  
    stateMachine.<>t__builder.Start<<RunAsync>d__2>(ref stateMachine);  
    return stateMachine.<>t__builder.Task;   //In original code there was no return statement but compiler added this line to return back to caller thread.
}

The compiler has created two state machine and both have try catch block but no return statement in the catch block so the error will propagate to surface. If we execute an asynchronous task inside the try catch block, we will get the exception: no need of continue with and then checking is faulted.

When we use the Task.Run exception, it will not be caught on try catch, because that will be shallowed by the task itself. We called those exceptions SHALLOWED EXCEPTIONS.

Let's conclude this topic with the advantages and disadvantages of using Async/Await...

Advantages

  • Increases efficiency when used properly
  • UI will be interactive
  • Easy to handle exceptions
  • Maximum utilization of CPU and Memory of system

Disadvantages

  • Every Async method add 100 bytes overhead to app
  • Deadlock will be around the corner
  • Increases complexity (without a proper understanding of things happening behind the scene)

Note

ASP.NET core does not have a Synchronization context, which means await defaults to the thread pool context. So, in the ASP.NET Core world, asynchronous may run on any thread, and they may all run in parallel.


Similar Articles