Async and Concurrency
C# async/await, Task, Thread vs C pthreads — modern concurrency patterns
Introduction
In this lesson, you'll learn about async and concurrency in C#. Coming from C, you already have a foundation for understanding this concept. We'll build on that knowledge while highlighting the key differences.
In C, you're familiar with c# async/await, task, thread vs c pthreads — modern concurrency patterns.
C# has its own approach to c# async/await, task, thread vs c pthreads — modern concurrency patterns, which we'll explore step by step.
The C# Way
Let's see how C# handles this concept. Here's a typical example:
using System;
using System.Threading;
using System.Threading.Tasks;
// async/await — non-blocking I/O (no threads blocked)
public async Task<string> FetchAsync(string url)
{
using var client = new HttpClient();
return await client.GetStringAsync(url); // non-blocking
}
// Run multiple async tasks in parallel
public async Task RunParallel()
{
var t1 = FetchAsync("https://api1.example.com");
var t2 = FetchAsync("https://api2.example.com");
string[] results = await Task.WhenAll(t1, t2);
Console.WriteLine(results[0]);
}
// Thread-safe counter
private int _counter = 0;
Interlocked.Increment(ref _counter); // atomic, no lock needed
// Mutex/lock for complex critical sections
private readonly object _lock = new();
lock (_lock)
{
_counter++;
// thread-safe block
}
// Parallel.ForEach for CPU-bound work
Parallel.ForEach(items, item => {
Process(item); // runs on thread pool
});
// Task.Run for CPU-bound work off the UI thread
var result = await Task.Run(() => ExpensiveComputation());Comparing to C
Here's how you might have written similar code in C:
#include <stdio.h>
#include <pthread.h>
#include <unistd.h>
// Shared data + mutex
int counter = 0;
pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
void *increment(void *arg) {
for (int i = 0; i < 1000; i++) {
pthread_mutex_lock(&lock);
counter++;
pthread_mutex_unlock(&lock);
}
return NULL;
}
// Parallel workers
int main(void) {
pthread_t t1, t2;
pthread_create(&t1, NULL, increment, NULL);
pthread_create(&t2, NULL, increment, NULL);
pthread_join(t1, NULL); // wait for t1
pthread_join(t2, NULL); // wait for t2
printf("counter=%d\n", counter); // 2000
pthread_mutex_destroy(&lock);
return 0;
}You may be used to different syntax or behavior.
async/await enables non-blocking I/O without threads; C needs pthreads for any concurrency
You may be used to different syntax or behavior.
Task<T> is a promise/future; await suspends the method without blocking a thread
You may be used to different syntax or behavior.
Interlocked.Increment is atomic; C needs pthread_mutex for the same
You may be used to different syntax or behavior.
lock() is simpler than pthread_mutex_lock/unlock — automatic unlock even on exception
You may be used to different syntax or behavior.
Task.WhenAll runs tasks in parallel and waits for all — no pthread_join loop
Step-by-Step Breakdown
1. async/await
async/await transforms I/O-bound code into non-blocking operations. No thread is blocked while waiting — unlike C's blocking read/recv.
// C: blocking recv() — thread stuck waiting
recv(sock, buf, len, 0);
process(buf);// C#: async — thread freed while awaiting I/O
public async Task<string> ReadAsync()
{
string data = await File.ReadAllTextAsync("f.txt");
return Process(data);
}2. Task.WhenAll
Task.WhenAll runs multiple tasks in parallel and waits for all. Much cleaner than creating pthreads and joining them.
pthread_t t1, t2;
pthread_create(&t1, NULL, fn1, NULL);
pthread_create(&t2, NULL, fn2, NULL);
pthread_join(t1, NULL); pthread_join(t2, NULL);var task1 = DoWork1Async();
var task2 = DoWork2Async();
await Task.WhenAll(task1, task2);
// both complete before continuing3. Thread Safety
lock {} auto-unlocks when the block exits — even on exception. Interlocked provides lock-free atomic operations for simple counters.
pthread_mutex_lock(&lock);
counter++;
pthread_mutex_unlock(&lock);
// What if exception? Lock stays locked!// Atomic counter — no lock needed
Interlocked.Increment(ref _counter);
// Complex section — auto-unlock guaranteed
lock (_lock) {
_data.Add(item);
_count++;
}4. Parallel.ForEach
Parallel.ForEach distributes CPU-bound work across thread pool threads. Much simpler than manual pthread management.
// C: create N threads, split work, join all
pthread_t threads[N];
for(int i=0;i<N;i++) pthread_create(&threads[i], NULL, fn, &chunks[i]);
for(int i=0;i<N;i++) pthread_join(threads[i], NULL);Parallel.ForEach(items, item => Process(item));
// Or with options:
Parallel.ForEach(items,
new ParallelOptions { MaxDegreeOfParallelism = 4 },
item => Process(item));Common Mistakes
When coming from C, developers often make these mistakes:
- async/await enables non-blocking I/O without threads; C needs pthreads for any concurrency
- Task<T> is a promise/future; await suspends the method without blocking a thread
- Interlocked.Increment is atomic; C needs pthread_mutex for the same
Key Takeaways
- async/await for I/O-bound work — no thread blocked while waiting
- Task.WhenAll for parallel fan-out — replaces pthread_create/join loop
- lock {} auto-unlocks (even on exception); Interlocked for atomic operations
- Parallel.ForEach for CPU-bound parallel work — no manual thread management