deepening into the Async Await in Dotnet Core

deepening into the Async Await in Dotnet Core

what are Async and await

Async and await are C# keywords that are used to specify and manage the asynchrony in your code. They enable you to write asynchronous code that looks and feels like synchronous code, making it easier to read and maintain.

Async methods are marked with the async keyword and contain at least one await expression. The await operator is applied to a task that represents an asynchronous operation, and it causes the method to pause its execution until the awaited task completes.

Here's an example of an async method that reads a file asynchronously and returns its contents as a string:

public async Task<string> ReadFileAsync(string filePath)
{
    using (var streamReader = new StreamReader(filePath))
    {
        return await streamReader.ReadToEndAsync();
    }
}

When the ReadToEndAsync method is called, the execution of the ReadFileAsync method is paused until the task returned by ReadToEndAsync completes. This allows other tasks to run in the meantime, rather than blocking the current thread.

As for why you should use async and await in your projects, there are a few benefits:

  1. Improved performance: Async code allows your application to make better use of system resources, because it can avoid blocking threads while waiting for asynchronous operations to complete.

  2. Better scalability: Async code can help your application handle a large number of concurrent requests more efficiently, because it can avoid creating too many threads.

  3. Easier to write and maintain: Async code is often easier to write and debug than asynchronous code that uses other techniques, such as manually using threads or the Task Parallel Library.

Deeping into the topic

thread pull

Async and await do not create or manage threads directly. Instead, they rely on the thread pool to execute asynchronous operations.

The thread pool is a system-wide service that provides a pool of worker threads that can be used to execute tasks asynchronously. When you call an async method, the runtime automatically schedules the continuation of the method on the thread pool when the awaited task completes.

Here's an example of what this might look like:

public async Task DoSomethingAsync()
{
    // Perform an asynchronous operation
    await SomeAsyncMethod();

    // The continuation of the method will be scheduled on the thread pool
    DoSomethingElse();
}

In this example, the DoSomethingElse method will be executed on a thread from the thread pool, rather than on the original calling thread.

One of the benefits of using async and await is that it allows your application to make better use of system resources, because it can avoid blocking threads while waiting for asynchronous operations to complete. This can help improve the performance and scalability of your application, especially when it needs to handle a large number of concurrent requests.

How does the compiler deal with Async Await

When you compile an async method, the compiler does the following:

  1. It translates the method into a state machine.

  2. It generates a new method that represents the state machine, and it replaces the original method with the new one.

For example, the ReadFileAsync method above would be transformed into something like this:

private sealed class ReadFileAsyncStateMachine : IAsyncStateMachine
{
    // State machine fields and methods go here
    public void MoveNext()
    {
        // State machine logic goes here
    }
}

The state machine is responsible for keeping track of the method's execution state and for resuming the method when the awaited task completes.

deep into the State Machine

As I mentioned earlier, when you compile an async method, the C# compiler translates the method into a state machine. This is an implementation detail that you generally don't need to worry about, but it's helpful to understand how it works because it can help you better understand how async and await work.

In C#, a state machine is simply a class that implements the IAsyncStateMachine interface. This interface has two members:

  1. MoveNext: This is a method that represents the logic of the async method. It's called repeatedly by the runtime to advance the state machine to its next state.

  2. SetStateMachine: This is a method that is used to set the instance of the state machine for a particular async method.

Here's an example of what a state machine for the ReadFileAsync method from earlier might look like:

private sealed class ReadFileAsyncStateMachine : IAsyncStateMachine
{
    // Fields to store the state of the method and its arguments
    private int _state;
    private TaskAwaiter<string> _awaiter;
    private string _result;

    public void MoveNext()
    {
        // Execute the next step of the method
        switch (_state)
        {
            case 0:
                // Open the file and get the TaskAwaiter
                using (var streamReader = new StreamReader(filePath))
                {
                    _awaiter = streamReader.ReadToEndAsync().GetAwaiter();
                    if (_awaiter.IsCompleted)
                    {
                        // If the task has already completed, move to the next state
                        _state = 1;
                        _result = _awaiter.GetResult();
                        return;
                    }
                }
                // If the task is not yet complete, register a callback to move to the next state when it does
                _state = 1;
                _awaiter.OnCompleted(MoveNext);
                break;
            case 1:
                // Get the result of the completed task and return it
                _result = _awaiter.GetResult();
                break;
        }
    }

    public void SetStateMachine(IAsyncStateMachine stateMachine)
    {
        // Set the instance of the state machine
        _stateMachine = stateMachine;
    }
}

The MoveNext method is responsible for advancing the async method through its various states of execution. It does this using a local variable to keep track of the current state. When the method reaches an await expression, it uses a TaskAwaiter to pause its execution and wait for the awaited Task to complete. Once the task completes, the MoveNext method is called again to resume the execution of the method. The TaskAwaiter also helps to manage the continuation of the method by registering a callback that is executed when the awaited task completes. This callback is responsible for advancing the method to its next state and for returning the result of the completed task, if applicable.

Let me know if you have any other questions.:)

Did you find this article valuable?

Support reza ghasemi by becoming a sponsor. Any amount is appreciated!