How async and await Improve Performance Over Manual Multithreading — With Fetch data API

DotNet Full Stack Dev
5 min readSep 16, 2024

When building high-performance applications, handling asynchronous tasks is a key requirement to ensure responsiveness and efficiency. Traditionally, developers have used manual multithreading to achieve concurrency, but modern approaches, like async and await in .NET, offer several advantages that make handling asynchronous operations simpler, safer, and more efficient.

In this blog post, we’ll dive into how async and await in C# improve performance compared to manual multithreading, using a real-world example to demonstrate the differences.

https://medium.com/@siva.veeravarapu/service-to-service-authentication-using-oauth2-in-net-microservices-product-and-order-services-56c0e00a2086

The Problem with Manual Multithreading

Before async and await were introduced, manual multithreading was the go-to solution for handling concurrent tasks. You would typically use Thread or ThreadPool to execute multiple tasks simultaneously. However, manual multithreading introduces several challenges:

1. Complexity in Managing Threads:

  • You need to create, manage, and control threads manually.
  • Threads consume system resources, and creating too many threads can overwhelm the system.
  • Thread synchronization (e.g., locks, semaphores) to prevent race conditions increases code complexity and the risk of deadlocks.

2. Blocking Threads:

  • Threads are blocked while waiting for I/O-bound operations (like fetching data from an external API or database).
  • This results in inefficient use of resources because the thread remains idle during these wait times.

3. Difficult Debugging:

  • Debugging and maintaining thread-heavy code is challenging due to potential race conditions, thread contention, and deadlocks.

The Introduction of async and await

In .NET, the async and await keywords were introduced to make asynchronous programming more intuitive and efficient. They allow you to write non-blocking code while abstracting away the complexity of manually managing threads.

Key Features:

  • Non-blocking calls: Unlike threads that wait (block) during I/O operations, async methods free up the main thread to do other work while waiting for the task to complete.
  • Efficient resource management: Threads are released during wait periods, and the task is resumed when the awaited result is available.
  • Simplified code: Asynchronous operations are expressed using clean, sequential code with async and await.

Let’s look at a real-world example to illustrate the difference between manual multithreading and using async and await.

Example Scenario: Calling a Third-Party API

Imagine an application that needs to call a third-party API to fetch data. This is an I/O-bound task, and we want to perform multiple calls concurrently to improve the overall performance.

Option 1: Manual Multithreading

In manual multithreading, we would use the Thread class to create new threads for each API call. Let’s take a look at an example.

Code: Using Manual Threads

public class ThirdPartyService
{
public void FetchDataUsingThreads()
{
Thread thread1 = new Thread(() => FetchDataFromAPI("https://api.example.com/data1"));
Thread thread2 = new Thread(() => FetchDataFromAPI("https://api.example.com/data2"));
Thread thread3 = new Thread(() => FetchDataFromAPI("https://api.example.com/data3"));

thread1.Start();
thread2.Start();
thread3.Start();

thread1.Join(); // Wait for thread1 to complete
thread2.Join(); // Wait for thread2 to complete
thread3.Join(); // Wait for thread3 to complete
}

private void FetchDataFromAPI(string url)
{
// Simulate network call
Thread.Sleep(2000); // Simulating a 2-second delay (API response time)
Console.WriteLine($"Data fetched from {url}");
}
}

Drawbacks of This Approach:

  • Blocking: Each thread is blocked while waiting for the API response (Thread.Sleep simulates this). Although we’ve created multiple threads, each is stuck waiting during the network call, consuming system resources.
  • Thread Management: We manually create and join threads, which increases code complexity.
  • Limited Scalability: Threads are expensive in terms of memory and system resources. If you need to scale this to hundreds or thousands of API calls, the application’s performance will degrade as it tries to manage more threads.

Option 2: Using async and await

Now, let’s refactor the same code using async and await to take advantage of non-blocking asynchronous calls.

Code: Using async and await

public class ThirdPartyService
{
public async Task FetchDataUsingAsyncAwait()
{
Task task1 = FetchDataFromAPIAsync("https://api.example.com/data1");
Task task2 = FetchDataFromAPIAsync("https://api.example.com/data2");
Task task3 = FetchDataFromAPIAsync("https://api.example.com/data3");

await Task.WhenAll(task1, task2, task3); // Await all tasks to complete
}

private async Task FetchDataFromAPIAsync(string url)
{
// Simulate asynchronous network call
await Task.Delay(2000); // Simulating a 2-second delay (API response time)
Console.WriteLine($"Data fetched from {url}");
}
}

Advantages of async and await:

  • Non-blocking: Unlike manual threading, the Task.Delay(2000) method simulates a non-blocking call. This means the application can continue doing other work while awaiting the API responses. The threads are not held up during the wait.
  • Simplified Code: We no longer need to manage threads manually. The async and await keywords handle the asynchronous nature in a more readable, sequential way.
  • Efficient Resource Usage: Once the await is hit, the current thread is freed up to do other work, and it doesn’t get blocked. The system reclaims resources while waiting for the API to respond.
  • Scalability: You can handle a large number of asynchronous operations without the overhead of managing hundreds or thousands of threads manually.

Performance Benefits of async and await

1. Non-blocking I/O Operations:

When using async and await, I/O-bound tasks such as file access, database queries, and API calls don’t block the thread. The application can continue executing other tasks while waiting for the result of the I/O operation. This leads to more efficient use of system resources and better performance in high-load scenarios.

In contrast, with manual multithreading, threads can be blocked during I/O operations, consuming resources unnecessarily and limiting performance.

2. Thread Management:

async and await rely on the .NET Task-based Asynchronous Pattern (TAP), which manages threads efficiently. Rather than creating multiple threads, tasks are scheduled to run on the thread pool, and threads are only used when necessary (i.e., when there’s actual CPU-bound work to do). This automatic thread management reduces overhead and makes the system more scalable.

With manual threading, you must explicitly create, start, and join threads. This can result in excessive thread creation, leading to increased memory usage and context switching, which degrades performance.

3. Easier Debugging and Maintenance:

Asynchronous code using async and await is much easier to read and maintain than manual multithreading. There’s no need for explicit thread management or synchronization mechanisms. The code is sequential, even though it’s asynchronous under the hood. This eliminates common threading issues like race conditions, deadlocks, and thread starvation.

Summary: Key Differences Between Manual Multithreading and async/await

Conclusion

Using async and await in .NET is a significant improvement over manual multithreading, especially when dealing with I/O-bound tasks. It provides a simpler, more efficient way to write asynchronous code, freeing up system resources during wait periods and allowing your application to scale more effectively.

By relying on async and await, you can achieve better performance, maintainability, and scalability without the headache of manually managing threads and dealing with the pitfalls of traditional multithreading. If you’re working with tasks like API calls, database queries, or file access, embracing asynchronous programming with async and await is the smarter choice.

--

--

DotNet Full Stack Dev
DotNet Full Stack Dev

Written by DotNet Full Stack Dev

Join me to master .NET Full Stack Development & boost your skills by 1% daily with insights, examples, and techniques! https://dotnet-fullstack-dev.blogspot.com

No responses yet