Solid Understanding of Async/Await and thread in .net

Adarsh Kumar
8 min readDec 4, 2023

--

1. Introduction

Before delving into the power of Async/Await, it’s crucial to establish a solid understanding of the .NET Runtime (CLR) and its foundational components. This involves comprehending the compilation of .NET code into CLR, which facilitates cross-platform execution. To build this foundation, we’ll explore essential terms such as thread pool, tasks, and task scheduler. This knowledge is pivotal for crafting efficient and error-resistant code.

2. .NET Runtime (CLR) Overview

The .NET Runtime provides essential services that contribute to the seamless execution of applications. Key functionalities include:

  • Just-In-Time (JIT) Compilation: Translating Intermediate Language (IL) code into native machine code at runtime.
  • Garbage Collection: Efficiently managing JVM memory.
  • Thread Management: Orchestrating the main application thread and additional threads, like those created for each API calls in web server, through the thread pool.
  • Assembly Loading and Reflection: Dynamically checking types and calling methods during runtime.
  • Security Services: Implementing code access security policies to regulate code permissions.

3. Thread Management in .NET Runtime

Thread Management is a critical aspect within the .NET Runtime, involving the coordination of various components such as the Thread Scheduler, ThreadPool, JIT Compiler, Garbage Collector, and synchronization mechanisms. These elements collaborate to offer a managed and efficient environment for the execution of multithreaded applications.

4. Understanding Thread Pool

To comprehend Thread Management, it’s vital to explore the Thread Pool, which is a fundamental component of the .NET Runtime. The Thread Pool Manager oversees the creation and lifecycle of threads. Two distinct types of pools exist within the thread pool: Worker Threads and Completion Port Threads.

Thread Pool Some basic components
1. Thread Pool Manager =>manage creation of thread and lifecycle of thread
2. There are two types of pool in thread pool, one is worker thread pool and Completion thread pool.
Worker Threads:
The primary entities in the Thread Pool are worker threads.
These threads are used to execute short-lived tasks in the application.
Completion Port Threads:
Completion port threads are associated with asynchronous I/O operations and work in conjunction with the I/O completion port mechanism.

Understanding their dynamics, including their dynamic sizing and developer-configurable limits, is crucial.

ThreadPool.GetMinThreads(out int minWorkerThreads, out int minCompletionPortThreads);
ThreadPool.GetMaxThreads(out int maxWorkerThreads, out int maxCompletionPortThreads);
Console.WriteLine($"Min Worker Threads: {minWorkerThreads}");
Console.WriteLine($"Max Worker Threads: {maxWorkerThreads}");
Console.WriteLine($"Min Completion Port Threads: {minCompletionPortThreads}");
Console.WriteLine($"Max Completion Port Threads: {maxCompletionPortThreads}");
This is Result of my Local setup of .net Core Application
Min Worker Threads: 8
Max Worker Threads: 32767
Min Completion Port Threads: 8
Max Completion Port Threads: 1000

5. Task Queue and Task Scheduler

The Thread Pool manages a queue of tasks awaiting execution. These tasks are placed in a Task Queue, and the Task Scheduler is responsible for efficiently scheduling these tasks to available threads.

Concisely
Task Queue:
The Thread Pool manages a queue of tasks that need to be executed.
When a task is submitted to the Thread Pool, it is placed in the task queue.
Task Scheduler:
The Task Scheduler is a component responsible for scheduling tasks from the queue to available threads.
It determines which worker thread or completion port thread should execute a particular task. Configuring the minimum and maximum pool sizes further enhances control over thread allocation.

ThreadPool.SetMinThreads(100, 300);
ThreadPool.SetMaxThreads(1000, 500);
First argument in both functions is the number of worker threads and
second is the number of Completion port threads.
If current request equal to 1000, and developers want to create a new thread, that request
remains in queue until the thread pool is available to allocate thread.

6. Async/Await Mechanism

Transitioning into the realm of Async/Await, it’s imperative to understand the async keyword’s role in C# methods. An async method indicates that it contains an operation with a time-consuming process dependent on external factors. This keyword enables the use of await and generates a state machine to manage asynchronous flows, promoting efficient handling without thread blocking.

The async keyword tells the compiler to treat the method in a special way, allowing it to use the await keyword and to generate a state machine to handle the asynchronous flow.
the async keyword is primarily used to denote methods that contain asynchronous operations, enabling the use of the await keyword and signaling that the method returns a Task or Task<T>. It doesn’t directly control the threads on which the method runs, but it enables asynchronous programming patterns that enable the efficient handling of asynchronous operations without blocking threads. It allows the method to yield control back to the caller during asynchronous waits, avoiding thread blocking and promoting better scalability and responsiveness.

async Task MyAsyncMethod()
{
// Asynchronous operations with await
}

7. The Power of Async

The async keyword empowers developers to create methods with asynchronous operations, leveraging the await keyword to facilitate non-blocking execution. This approach enhances scalability, responsiveness, and overall code efficiency by allowing methods to yield control during asynchronous waits, preventing thread blocking.

By establishing a comprehensive understanding of these foundational concepts, developers can harness the full power of Async/Await for robust and efficient code development.

Now Comes with some example

  1. Scenario: Basic Async/Await Flow
using System;
using System.Threading.Tasks;
using Newtonsoft.Json;

class Program
{
static async void asyncTask()
{
Console.WriteLine($"Before await thread is working on it CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)} ");
await Task.Delay(10000);
Console.WriteLine($"After await thread is working on it CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)}");
}
static void Main(string[] args)
{

Console.WriteLine($"Before asyncTask() method called the Main Thread is running CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)}");
asyncTask();
Console.WriteLine($"After asyncTask() Thread is running CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)}");
}
}
Before asyncTask() method called the Main Thread is running CurrentThreadInfo: {"ManagedThreadId":1,"IsAlive":true,"IsBackground":false,"IsThreadPoolThread":false,"Priority":2,"ThreadState":0,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}
Before await thread is working on it CurrentThreadInfo: {"ManagedThreadId":1,"IsAlive":true,"IsBackground":false,"IsThreadPoolThread":false,"Priority":2,"ThreadState":0,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}
After asyncTask() Thread is running CurrentThreadInfo: {"ManagedThreadId":1,"IsAlive":true,"IsBackground":false,"IsThreadPoolThread":false,"Priority":2,"ThreadState":0,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}

After await thread is working on it CurrentThreadInfo: {"ManagedThreadId":8,"IsAlive":true,"IsBackground":true,"IsThreadPoolThread":true,"Priority":2,"ThreadState":4,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":".NET ThreadPool Worker","ApartmentState":1}

In this scenario, we have a simple asynchronous method, asyncTask(), using async and await with Task.Delay(10000). The key observations are as follows:

  • Main Thread Identification: The Main Thread, identified by its Managed ThreadId 1, runs before and after the invocation of asyncTask(). The entire process occurs on the same thread. It allows the method to yield control back to the caller during asynchronous waits.
  • Async Operation Thread: During the asynchronous delay, a new thread from the Thread Pool (Managed ThreadId: 8) takes over. This demonstrates the non-blocking nature of async/await, allowing the Main Thread to continue execution.

This scenario showcases the seamless transition between threads during an asynchronous operation, highlighting the responsiveness of the application.

2. Scenario: Synchronous Async/Await

using System;
using Newtonsoft.Json;

class Program
{
static async void asyncTask()
{
Console.WriteLine($"Before await thread is working on it CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)} ");
Console.WriteLine($"After await thread is working on it CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)}");
}
static void Main(string[] args)
{

Console.WriteLine($"Before asyncTask() method called the Main Thread is running CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)}");
asyncTask();
Console.WriteLine($"After asyncTask() Thread is running CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)}");
}
}
Before asyncTask() method called the Main Thread is running CurrentThreadInfo: {"ManagedThreadId":1,"IsAlive":true,"IsBackground":false,"IsThreadPoolThread":false,"Priority":2,"ThreadState":0,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}
Before await thread is working on it CurrentThreadInfo: {"ManagedThreadId":1,"IsAlive":true,"IsBackground":false,"IsThreadPoolThread":false,"Priority":2,"ThreadState":0,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}
After await thread is working on it CurrentThreadInfo: {"ManagedThreadId":1,"IsAlive":true,"IsBackground":false,"IsThreadPoolThread":false,"Priority":2,"ThreadState":0,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}
After asyncTask() Thread is running CurrentThreadInfo: {"ManagedThreadId":1,"IsAlive":true,"IsBackground":false,"IsThreadPoolThread":false,"Priority":2,"ThreadState":0,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}

Here, asyncTask() contains only synchronous operations, revealing an interesting behavior:

  • Main Thread Continuity: The Main Thread (ManagedThreadId: 1) remains consistent before, during, and after the invocation of asyncTask(). No additional thread from the ThreadPool is introduced.

This scenario emphasizes that not all methods marked as async involve thread-switching, and the absence of asynchronous operations results in a straightforward, synchronous execution on the calling thread.

3. Scenario: Missing Await in Async Method

using System;
using System.Threading.Tasks;
using Newtonsoft.Json;

class Program
{
static async void asyncTask()
{
Console.WriteLine($"Before await thread is working on it CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)} ");
Task.Delay(10000);
Console.WriteLine($"After await thread is working on it CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)}");
}
static void Main(string[] args)
{

Console.WriteLine($"Before asyncTask() method called the Main Thread is running CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)}");
asyncTask();
Console.WriteLine($"After asyncTask() Thread is running CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)}");
}
}
Before asyncTask() method called the Main Thread is running CurrentThreadInfo: {"ManagedThreadId":1,"IsAlive":true,"IsBackground":false,"IsThreadPoolThread":false,"Priority":2,"ThreadState":0,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}
Before await thread is working on it CurrentThreadInfo: {"ManagedThreadId":1,"IsAlive":true,"IsBackground":false,"IsThreadPoolThread":false,"Priority":2,"ThreadState":0,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}
After await thread is working on it CurrentThreadInfo: {"ManagedThreadId":1,"IsAlive":true,"IsBackground":false,"IsThreadPoolThread":false,"Priority":2,"ThreadState":0,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}
After asyncTask() Thread is running CurrentThreadInfo: {"ManagedThreadId":1,"IsAlive":true,"IsBackground":false,"IsThreadPoolThread":false,"Priority":2,"ThreadState":0,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}

In this case, Task.Delay(10000) lacks the await keyword within asyncTask(). The observations are:

  • Main Thread Consistency: The Main Thread (Managed ThreadId: 1) persists throughout the entire process, both before and after asyncTask().
  • Async Operation Ignored: Despite the method being marked as async, the absence of await causes the asynchronous operation to be ignored, resulting in synchronous behavior. here thread id 1 wait for 10000 ms before printing “After await thread is working”.

This scenario underscores the importance of properly utilizing await to enable the desired asynchronous behavior.

4. Scenario: Custom Thread with Thread.Sleep

using System;
using System.Threading;
using Newtonsoft.Json;

class Program
{
static async void asyncTask()
{
Console.WriteLine($"Before await thread is working on it CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)} ");
Thread.Sleep(10000);
Console.WriteLine($"After await thread is working on it CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)}");
}
static void Main(string[] args)
{

Console.WriteLine($"Before asyncTask() method called the Main Thread is running CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)}");
Thread customThread = new Thread(asyncTask);
customThread.IsBackground = true;
customThread.Start();
Console.WriteLine($"After asyncTask() Thread is running CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)}");
}
}
Before asyncTask() method called the Main Thread is running CurrentThreadInfo: {"ManagedThreadId":1,"IsAlive":true,"IsBackground":false,"IsThreadPoolThread":false,"Priority":2,"ThreadState":0,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}
After asyncTask() Thread is running CurrentThreadInfo: {"ManagedThreadId":1,"IsAlive":true,"IsBackground":false,"IsThreadPoolThread":false,"Priority":2,"ThreadState":0,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}
Before await thread is working on it CurrentThreadInfo: {"ManagedThreadId":10,"IsAlive":true,"IsBackground":true,"IsThreadPoolThread":false,"Priority":2,"ThreadState":4,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}
After await thread is working on it CurrentThreadInfo: {"ManagedThreadId":10,"IsAlive":true,"IsBackground":true,"IsThreadPoolThread":false,"Priority":2,"ThreadState":4,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}

Here, a custom thread is created using new Thread() to execute asyncTask(), and a Thread.Sleep(1000) is introduced:

  • Main Thread Independence: The Main Thread (ManagedThreadId: 1) remains unaffected by the custom thread, allowing it to proceed independently.
  • Custom Thread Overhead: Creating a custom thread introduces overhead, visible in the delay between starting the custom thread and its actual execution.

This scenario illustrates the additional complexities associated with custom thread creation, emphasizing the trade-offs between control and overhead.

5. Scenario: Custom Thread with Main Thread Interaction

using System;
using System.Threading;
using Newtonsoft.Json;

class Program
{
static async void asyncTask()
{
Console.WriteLine($"Before await thread is working on it CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)} ");
Thread.Sleep(10000);
Console.WriteLine($"After await thread is working on it CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)}");
}
static void Main(string[] args)
{

Console.WriteLine($"Before asyncTask() method called the Main Thread is running CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)}");
Thread customThread = new Thread(asyncTask);
customThread.Start();
Thread.Sleep(1000);
Console.WriteLine($"After asyncTask() Thread is running CurrentThreadInfo: {JsonConvert.SerializeObject(Thread.CurrentThread)}");
}
}
Before asyncTask() method called the Main Thread is running CurrentThreadInfo: {"ManagedThreadId":1,"IsAlive":true,"IsBackground":false,"IsThreadPoolThread":false,"Priority":2,"ThreadState":0,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}
Before await thread is working on it CurrentThreadInfo: {"ManagedThreadId":12,"IsAlive":true,"IsBackground":false,"IsThreadPoolThread":false,"Priority":2,"ThreadState":0,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}
After asyncTask() Thread is running CurrentThreadInfo: {"ManagedThreadId":1,"IsAlive":true,"IsBackground":false,"IsThreadPoolThread":false,"Priority":2,"ThreadState":0,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}
After await thread is working on it CurrentThreadInfo: {"ManagedThreadId":12,"IsAlive":true,"IsBackground":false,"IsThreadPoolThread":false,"Priority":2,"ThreadState":0,"CurrentCulture":"en-IN","CurrentUICulture":"en-US","ExecutionContext":{},"Name":null,"ApartmentState":1}

In this final scenario, a custom thread runs asyncTask() while the Main Thread sleeps briefly:

  • Main Thread Interaction: The Main Thread (ManagedThreadId: 1) and the custom thread (ManagedThreadId: 12) operate concurrently, demonstrating parallelism.
  • Async Operation Execution: Despite the Main Thread sleeping, the custom thread executes the asynchronous operation, showcasing the asynchronous nature of the operation.

This scenario highlights the potential for parallel execution and the importance of understanding the interplay between multiple threads in a concurrent environment.

In conclusion, these scenarios provide insights into the behavior of threads in various asynchronous scenarios, helping developers navigate the intricacies of multithreading in C# applications.

--

--