What is Concurrency?
Concurrency means dealing with more than one task during the same period of time. The tasks are all "in progress" together, but they do not have to run at exactly the same instant.
Think of one chef cooking three dishes. The chef puts rice on the stove, then chops vegetables while the rice cooks, then stirs the curry, then checks the rice again. Only one pair of hands is working, but all three dishes are moving forward at the same time. That is concurrency.
Concurrency is about switching and overlapping
A concurrent program can make progress on many tasks in two ways:
- Switching: one worker moves quickly between tasks (like the chef). On a computer, even a single CPU core can switch between threads so fast that they all seem to run together.
- Overlapping waiting time: while one task is waiting (for the database, the network, the disk), another task can use the worker. This is exactly what
asyncandawaitdo.
Concurrency with async and await
Say a dashboard page needs customers, orders and products from three different services. Each call takes time. If we call them one by one, the user waits for the total. If we call them concurrently, the user waits only for the slowest one.
using System;
using System.Diagnostics;
using System.Threading.Tasks;
class Program
{
static async Task Main()
{
Stopwatch watch = Stopwatch.StartNew();
// Start all three calls. They are now running concurrently.
Task<string> customersTask = GetDataAsync("Customers", 2000);
Task<string> ordersTask = GetDataAsync("Orders", 3000);
Task<string> productsTask = GetDataAsync("Products", 1000);
// Wait for all of them.
string[] results = await Task.WhenAll(customersTask, ordersTask, productsTask);
foreach (string result in results)
{
Console.WriteLine(result);
}
Console.WriteLine($"Total time: {watch.Elapsed.TotalSeconds:F0} seconds");
}
static async Task<string> GetDataAsync(string name, int milliseconds)
{
Console.WriteLine($"Loading {name}...");
await Task.Delay(milliseconds); // pretend this is an API or database call
return $"{name} loaded";
}
}
One after another, this would take 2 + 3 + 1 = 6 seconds. Concurrently it takes about 3 seconds. Task.WhenAll returns the results in the same order as the tasks you passed in, not in the order they finished.
During Task.Delay (or a real database call), no thread is waiting. That is why async concurrency is so cheap: you can have thousands of calls in progress without thousands of threads.
Real-world examples of concurrency
- A web server handling requests from many users at the same time.
- A desktop app that downloads a file while you keep typing in it.
- Calling several microservices together and combining their results.
- Your operating system running a browser, music player and Visual Studio together on a laptop.
The challenge of concurrency: shared data
When several tasks use the same data at the same time, the result can be wrong. This is called a race condition.
int counter = 0;
// 10 tasks, each adds 1 to the same counter 100,000 times
Task[] tasks = new Task[10];
for (int i = 0; i < tasks.Length; i++)
{
tasks[i] = Task.Run(() =>
{
for (int j = 0; j < 100_000; j++)
{
counter++;
}
});
}
await Task.WhenAll(tasks);
Console.WriteLine(counter); // expected 1000000, but often prints less, e.g. 412873
counter++ is really three steps: read, add 1, write. Two threads can read the same value at the same moment and both write back the same number, so one increment is lost. Fix it by making the update safe:
Interlocked.Increment(ref counter); // safe, one thread at a time
// or
lock (locker)
{
counter++;
}
For a beginner the main point is: concurrency is powerful, but tasks that share data need protection.
Summary
- Concurrency means several tasks are in progress during the same period of time.
- They do not need to run at the same instant; one worker can switch between them.
async/awaitwithTask.WhenAllis the easiest way to get concurrency for waiting work in C#.- Concurrent waiting work finishes in about the time of the slowest task.
- When tasks share data, protect it with
lockorInterlockedto avoid race conditions.