Asynchronous Programming
Concurrent Execution
Parallel Processing
Multithreading
Software Development

Run multiple instances of same method asynchronously?

Master System Design with Codemia

Enhance your system design skills with over 120 practice problems, detailed solutions, and hands-on exercises.

Introduction

Running the same method multiple times asynchronously is usually a question of scheduling several independent tasks and then waiting for all of them to complete. The exact tool depends on whether the work is I/O-bound or CPU-bound, but the core idea is the same: create multiple units of work, launch them without blocking one another, and coordinate the results safely.

The Basic Pattern

In C#, the usual shape is:

  1. define a method that can run independently
  2. start several tasks
  3. wait for them with Task.WhenAll

For example:

csharp
1using System;
2using System.Linq;
3using System.Threading.Tasks;
4
5class Program
6{
7    static async Task<int> FetchValueAsync(int id)
8    {
9        await Task.Delay(500);
10        return id * 10;
11    }
12
13    static async Task Main()
14    {
15        var tasks = Enumerable.Range(1, 5)
16            .Select(FetchValueAsync)
17            .ToArray();
18
19        int[] results = await Task.WhenAll(tasks);
20        Console.WriteLine(string.Join(", ", results));
21    }
22}

This runs five logical operations concurrently and resumes once all five are finished.

Asynchronous Does Not Always Mean Parallel Threads

A common misunderstanding is thinking asynchronous execution always means a new thread per call. That is not true.

If the method is truly asynchronous and I/O-bound, such as waiting on HTTP or database responses, the runtime can overlap many calls efficiently without dedicating one thread to each waiting operation.

If the work is CPU-bound, you may need explicit task scheduling on the thread pool instead.

csharp
1using System;
2using System.Linq;
3using System.Threading.Tasks;
4
5class Program
6{
7    static int Compute(int n)
8    {
9        int total = 0;
10        for (int i = 0; i < 1_000_000; i++)
11        {
12            total += (n + i) % 7;
13        }
14        return total;
15    }
16
17    static async Task Main()
18    {
19        var tasks = Enumerable.Range(1, 4)
20            .Select(n => Task.Run(() => Compute(n)))
21            .ToArray();
22
23        int[] results = await Task.WhenAll(tasks);
24        Console.WriteLine(string.Join(", ", results));
25    }
26}

Here Task.Run is reasonable because the work is CPU-heavy. For naturally asynchronous I/O methods, wrapping everything in Task.Run is often unnecessary.

Be Careful With Shared State

The method can only be run safely in parallel if each invocation is independent or the shared state is protected.

For example, this is risky:

csharp
1static int counter = 0;
2
3static async Task IncrementAsync()
4{
5    int snapshot = counter;
6    await Task.Delay(10);
7    counter = snapshot + 1;
8}

If many calls run at once, updates can be lost. Safer designs include:

  • returning values instead of mutating shared state
  • using thread-safe structures
  • locking only when truly necessary

A better pattern is:

csharp
1static async Task<int> WorkAsync(int input)
2{
3    await Task.Delay(100);
4    return input + 1;
5}

Then aggregate results after Task.WhenAll.

Limit Concurrency When Needed

Starting one hundred or one thousand operations at once is not always wise. External systems such as APIs, databases, and file handles often have limits.

SemaphoreSlim is a common way to throttle concurrency:

csharp
1using System;
2using System.Linq;
3using System.Threading;
4using System.Threading.Tasks;
5
6static SemaphoreSlim gate = new SemaphoreSlim(3);
7
8static async Task<int> LimitedWorkAsync(int id)
9{
10    await gate.WaitAsync();
11    try
12    {
13        await Task.Delay(300);
14        return id * id;
15    }
16    finally
17    {
18        gate.Release();
19    }
20}

That pattern is especially useful for outbound API calls.

Exceptions and Cancellation

When you run many instances asynchronously, failure handling becomes more important. Task.WhenAll throws if any task fails, so you should plan for that.

You may also want cancellation support:

csharp
1static async Task<int> FetchAsync(int id, CancellationToken token)
2{
3    await Task.Delay(500, token);
4    return id;
5}

Cooperative cancellation keeps large task groups manageable when the caller no longer cares about the result.

Common Pitfalls

The most common mistake is calling the same method repeatedly but awaiting each call immediately inside the loop. That makes the code sequential, not concurrent.

Another mistake is using Task.Run for everything. For naturally asynchronous I/O methods, just calling the async method and collecting the tasks is usually enough.

Developers also often forget about shared mutable state. A method that is correct when called once can become wrong when several instances run at the same time.

Finally, unbounded concurrency can overwhelm a downstream system. Sometimes the right answer is not "run everything at once" but "run a controlled number at once."

Summary

  • Start multiple independent tasks and await them together with Task.WhenAll.
  • Use true async methods for I/O-bound work and Task.Run only when CPU-bound work justifies it.
  • Avoid unsafe shared mutable state across concurrent method calls.
  • Throttle concurrency when external systems or resources have limits.
  • Handle exceptions and cancellation deliberately when many tasks are in flight.

Course illustration
Course illustration

All Rights Reserved.