Master modern C# concurrency by understanding the differences between Concurrency (overlapping time periods) and Parallelism (simultaneous execution on multiple cores).
Learn to leverage the Task-based Asynchronous Pattern (TAP) with async/await for non-blocking I/O-bound work, and use the Parallel class and PLINQ for CPU-bound computations.
The guide also details thread safety with lock, Interlocked, and Concurrent Collections, and explores advanced topics like CancellationTokenand Async Streams (IAsyncEnumerable), complete with best practices and common pitfalls.
🎯 The Big Picture: How Everything Connects
┌─────────────────────────────────────────────────────────┐
│ CONCURRENCY │
│ (Doing multiple things in overlapping time periods) │
│ │
│ ┌──────────────────────┐ ┌──────────────────────┐ │
│ │ PARALLELISM │ │ ASYNCHRONOUS │ │
│ │ (True simultaneous │ │ (Non-blocking, │ │
│ │ execution on │ │ waiting without │ │
│ │ multiple cores) │ │ blocking threads) │ │
│ │ │ │ │ │
│ │ • Parallel class │ │ • async/await │ │
│ │ • PLINQ │ │ • Task (I/O bound) │ │
│ │ • Task (CPU bound) │ │ • TAP pattern │ │
│ └──────────────────────┘ └──────────────────────┘ │
│ │
│ BOTH Built on: Thread & Task │
└─────────────────────────────────────────────────────────┘
Key Relationships
- Thread: OS-level execution unit (expensive, limited)
- Task: High-level abstraction over threads (cheap, efficient)
- async/await: Language feature to write asynchronous code that looks synchronous
- Parallel: For CPU-intensive work across multiple cores
- Concurrency: Umbrella term for all of the above
📚 Core Concepts Explained
Synchronous vs Asynchronous
Synchronous: Code executes line-by-line, blocking until each operation completes.
string data = DownloadData(); // Waits here, blocks thread
ProcessData(data);
Asynchronous: Code can continue without waiting for operation to complete.
Task<string> dataTask = DownloadDataAsync(); // Starts and returns immediately
// Do other work here
string data = await dataTask; // Wait only when you need the result
ProcessData(data);
Blocking vs Non-Blocking
Blocking: Thread sits idle, waiting for operation (wasteful).
- Example:
Thread.Sleep(),Task.Wait(),Task.Result
Non-Blocking: Thread is released to do other work while waiting.
- Example:
await,ContinueWith()
CPU-Bound vs I/O-Bound
CPU-Bound: Work that requires computation (math, processing, encryption).
- Use:
Task.Run(),Parallel.For(), multi-threading
I/O-Bound: Work that waits for external resources (file, network, database).
- Use:
async/await, asynchronous methods
Thread (Legacy Approach)
What it is: Low-level OS thread for executing code concurrently.
Use: Legacy code or when you need absolute control (rare in modern C#).
Modern alternative: Use Task instead.
Key Properties
-
IsAlive- Boolean if thread is running -
IsBackground- If true, doesn't prevent app from terminating -
Name- Thread name for debugging -
Priority- ThreadPriority enum (Lowest to Highest) -
ThreadState- Current state (Running, Stopped, etc.) -
ManagedThreadId- Unique ID
Key Methods
-
Start()- Begin execution -
Join()- Block until thread completes -
Join(timeout)- Wait with timeout -
Abort()- ⚠️ DEPRECATED: Force stop (dangerous!) -
Sleep(ms)- Pause current thread -
Interrupt()- Interrupt waiting thread
Static Methods
-
Thread.CurrentThread- Get current thread -
Thread.Sleep(ms)- Pause current thread -
Thread.Yield()- Give up CPU to other threads
Example (Legacy)
Thread thread = new Thread(() => {
Console.WriteLine("Running in background");
});
thread.IsBackground = true;
thread.Start();
thread.Join(); // Wait for completion
⚠️ Problems with Threads
- Expensive (1MB stack per thread)
- Limited number available
- Hard to manage
- No easy way to get return values
- No built-in error handling
Task (Modern Approach)
What it is: High-level abstraction representing an asynchronous operation.
Use: Default choice for all async/concurrent work in modern C#.
Benefit: Lightweight, composable, better error handling, works with async/await.
Key Properties
-
Status- Current state (Running, RanToCompletion, Faulted, Canceled) -
IsCompleted- Boolean if finished (success, fault, or cancel) -
IsCompletedSuccessfully- Boolean if finished successfully -
IsFaulted- Boolean if threw exception -
IsCanceled- Boolean if was canceled -
Exception- AggregateException if faulted -
Result- Return value (⚠️ blocks until complete) -
Id- Unique identifier
Key Methods
-
Wait()- ⚠️ Block until complete (avoid, use await) -
Wait(timeout)- Block with timeout -
ContinueWith(action)- Chain another task -
GetAwaiter()- Get awaiter (used by await keyword) -
ConfigureAwait(false)- Don't capture context
Static Methods
-
Task.Run(action)- Run code on thread pool -
Task.Delay(ms)- Async delay (like async Sleep) -
Task.WhenAll(tasks)- Wait for all tasks -
Task.WhenAny(tasks)- Wait for first task -
Task.FromResult(value)- Create completed task -
Task.FromException(ex)- Create faulted task -
Task.FromCanceled(token)- Create canceled task -
Task.CompletedTask- Pre-completed task
Task vs Task<T>
-
Task- No return value (like void) -
Task<T>- Returns value of type T
Example
// Start CPU-bound work
Task<int> task = Task.Run(() => {
// Heavy computation
return 42;
});
// Do other work...
// Get result when needed
int result = await task;
async / await (The Game Changer)
What it is: C# language keywords that make asynchronous code look synchronous.
Use: Any I/O-bound operation (file, network, database).
Benefit: Non-blocking, easy to read, automatic error handling.
Rules & Syntax
// Method signature
public async Task<string> DownloadDataAsync()
{
string data = await httpClient.GetStringAsync(url);
return data; // Returns Task<string> automatically
}
// Calling async method
string result = await DownloadDataAsync();
Key Rules
- Methods with
awaitmust be markedasync - Async methods should return
TaskorTask<T>(orValueTask<T>) - By convention, name async methods with "Async" suffix
- Cannot use
awaitin:- Synchronous methods
-
catch/finallyblocks (pre-C# 6) - Lock statements
- Unsafe code
Return Types
-
Task- Async method with no return value -
Task<T>- Async method returning T -
ValueTask<T>- Performance optimization (avoid allocations) -
void- ⚠️ Only for event handlers (no error handling!) -
IAsyncEnumerable<T>- Async streams (C# 8.0+)
Example: Async vs Sync
// ❌ Synchronous (blocks thread)
public string GetData()
{
Thread.Sleep(1000); // Thread sits idle
return "data";
}
// ✅ Asynchronous (releases thread)
public async Task<string> GetDataAsync()
{
await Task.Delay(1000); // Thread does other work
return "data";
}
ConfigureAwait
// Capture context (default) - for UI apps
await SomeMethodAsync();
// Don't capture context - for libraries, better performance
await SomeMethodAsync().ConfigureAwait(false);
TAP (Task-based Asynchronous Pattern) ⭐ Modern Standard
What it is: The recommended pattern for async programming in .NET.
Use: All new asynchronous APIs should follow this pattern.
TAP Rules
- Method returns
TaskorTask<T> - Method name ends with "Async"
- Method should have an overload accepting
CancellationToken - Should be truly asynchronous (not just wrapping sync code)
Example
public async Task<string> ReadFileAsync(string path,
CancellationToken cancellationToken = default)
{
using (StreamReader reader = new StreamReader(path))
{
return await reader.ReadToEndAsync(cancellationToken);
}
}
TAP vs Other Patterns
| Pattern | Era | Example |
|---|---|---|
| TAP ⭐ | Modern (.NET 4.5+) | Task<T> MethodAsync() |
| APM | Legacy (.NET 1.0) |
IAsyncResult BeginMethod() + EndMethod()
|
| EAP | Legacy (.NET 2.0) |
MethodAsync() + MethodCompleted event |
Always use TAP for new code.
Parallel Programming (CPU-Bound Work)
What it is: Execute CPU-intensive work across multiple cores simultaneously.
Use: When you have work that can be divided and processed independently.
Don't use for: I/O-bound work (use async/await instead).
Parallel Class
What it is: Static class for parallel loops and invocations.
Use: Process collections in parallel.
Key Methods
-
Parallel.For(from, to, action)- Parallel for loop -
Parallel.ForEach(collection, action)- Parallel foreach -
Parallel.Invoke(actions)- Execute multiple actions in parallel
Example
// Sequential (slow)
for (int i = 0; i < 1000; i++)
{
ProcessItem(i);
}
// Parallel (fast)
Parallel.For(0, 1000, i => {
ProcessItem(i);
});
// Parallel ForEach
Parallel.ForEach(items, item => {
ProcessItem(item);
});
// Execute multiple methods in parallel
Parallel.Invoke(
() => Method1(),
() => Method2(),
() => Method3()
);
ParallelOptions
var options = new ParallelOptions
{
MaxDegreeOfParallelism = 4, // Limit to 4 threads
CancellationToken = cancellationToken
};
Parallel.ForEach(items, options, item => {
ProcessItem(item);
});
PLINQ (Parallel LINQ)
What it is: Parallel version of LINQ for query processing.
Use: Parallelize LINQ queries on large datasets.
Key Methods
-
.AsParallel()- Convert to parallel query -
.AsOrdered()- Maintain order (slower) -
.WithDegreeOfParallelism(n)- Limit parallelism -
.WithCancellation(token)- Enable cancellation -
.ForAll(action)- Parallel action on results
Example
// Sequential LINQ
var results = items
.Where(x => x.IsValid)
.Select(x => ProcessItem(x))
.ToList();
// Parallel LINQ
var results = items
.AsParallel()
.Where(x => x.IsValid)
.Select(x => ProcessItem(x))
.ToList();
// With options
var results = items
.AsParallel()
.WithDegreeOfParallelism(4)
.WithCancellation(cancellationToken)
.Where(x => x.IsValid)
.Select(x => ProcessItem(x))
.ToList();
Partitioner
What it is: Controls how work is divided among threads.
Use: Optimize parallel performance for specific scenarios.
var partitioner = Partitioner.Create(0, items.Length, 100);
Parallel.ForEach(partitioner, range => {
for (int i = range.Item1; i < range.Item2; i++)
{
ProcessItem(items[i]);
}
});
CancellationToken & CancellationTokenSource
What it is: Cooperative cancellation mechanism for async operations.
Use: Allow users to cancel long-running operations.
CancellationTokenSource
What it is: Creates and controls cancellation tokens.
Key Properties
-
Token- Get CancellationToken -
IsCancellationRequested- Boolean if cancellation requested
Key Methods
-
Cancel()- Request cancellation -
CancelAfter(ms)- Auto-cancel after timeout -
Dispose()- Release resources
CancellationToken
What it is: Token passed to async methods to check for cancellation.
Key Properties
-
IsCancellationRequested- Boolean if canceled -
CanBeCanceled- Boolean if can be canceled
Key Methods
-
ThrowIfCancellationRequested()- Throw if canceled -
Register(callback)- Execute callback on cancellation
Example
CancellationTokenSource cts = new CancellationTokenSource();
// Pass token to async method
Task task = LongRunningOperationAsync(cts.Token);
// Cancel after 5 seconds
cts.CancelAfter(5000);
// Or cancel immediately
// cts.Cancel();
try
{
await task;
}
catch (OperationCanceledException)
{
Console.WriteLine("Operation canceled");
}
// In the async method
async Task LongRunningOperationAsync(CancellationToken token)
{
for (int i = 0; i < 1000; i++)
{
token.ThrowIfCancellationRequested();
await Task.Delay(100, token);
}
}
ThreadPool
What it is: Managed pool of worker threads reused for tasks.
Use: Understanding what's under the hood (Task uses this internally).
Direct use: Rarely needed in modern C#.
Key Static Methods
-
QueueUserWorkItem(callback)- Queue work -
GetMaxThreads(out worker, out io)- Get thread limits -
SetMaxThreads(worker, io)- Set thread limits -
GetAvailableThreads(out worker, out io)- Get available threads
Example (Legacy)
// Legacy approach
ThreadPool.QueueUserWorkItem(state => {
Console.WriteLine("Work item");
});
// Modern approach (same thing)
Task.Run(() => {
Console.WriteLine("Work item");
});
SynchronizationContext
What it is: Abstraction for scheduling work back to original context (UI thread).
Use: Understanding how async/await works with UI frameworks.
Direct use: Rare, handled automatically by async/await.
Key Scenarios
- UI apps (WPF, WinForms): Await resumes on UI thread
- ASP.NET (legacy): Await resumes on request context
- ASP.NET Core: No context (better performance)
- Console apps: No context
ConfigureAwait Explained
// Capture context (default)
await SomeMethodAsync(); // Resumes on original context (UI thread)
// Don't capture context
await SomeMethodAsync().ConfigureAwait(false); // Can resume on any thread
Rule of thumb:
- UI code: Use default (capture context)
- Library code: Use
ConfigureAwait(false)(better performance)
Event-Driven Programming
What it is: Respond to events/notifications rather than polling.
Use: UI interactions, file system monitoring, message queues.
Events and Delegates
Delegate
public delegate void NotificationHandler(string message);
Event
public class Publisher
{
public event NotificationHandler OnNotification;
public void Notify(string message)
{
OnNotification?.Invoke(message);
}
}
// Usage
Publisher pub = new Publisher();
pub.OnNotification += (msg) => Console.WriteLine(msg);
pub.Notify("Hello!");
Async Event Handlers
public event Func<string, Task> OnNotificationAsync;
public async Task NotifyAsync(string message)
{
if (OnNotificationAsync != null)
{
await OnNotificationAsync(message);
}
}
// Usage
pub.OnNotificationAsync += async (msg) => {
await Task.Delay(100);
Console.WriteLine(msg);
};
Built-in Event Pattern
public class Publisher
{
public event EventHandler<MessageEventArgs> MessageReceived;
protected virtual void OnMessageReceived(MessageEventArgs e)
{
MessageReceived?.Invoke(this, e);
}
}
public class MessageEventArgs : EventArgs
{
public string Message { get; set; }
}
Thread Safety & Synchronization
The Problem: Race Conditions
int counter = 0;
// ❌ NOT thread-safe
Parallel.For(0, 1000, i => {
counter++; // Race condition!
});
Solution 1: lock (Monitor)
What it is: Ensures only one thread can execute code block at a time.
Use: Protect shared state in multi-threaded code.
object lockObj = new object();
int counter = 0;
Parallel.For(0, 1000, i => {
lock (lockObj)
{
counter++; // Safe
}
});
Key Points
- Lock on private object, never public
- Keep locked sections small
- Never lock on
this,typeof(MyClass), or strings - Can cause deadlocks if not careful
Solution 2: Interlocked
What it is: Atomic operations for simple types.
Use: When you only need to increment/decrement/compare-exchange.
Key Methods
-
Interlocked.Increment(ref value)- Atomic increment -
Interlocked.Decrement(ref value)- Atomic decrement -
Interlocked.Add(ref location, value)- Atomic add -
Interlocked.Exchange(ref location, value)- Atomic set -
Interlocked.CompareExchange(ref location, value, comparand)- Atomic compare and set
int counter = 0;
Parallel.For(0, 1000, i => {
Interlocked.Increment(ref counter); // Safe and fast
});
Solution 3: Concurrent Collections
What it is: Thread-safe collection classes.
Use: Shared collections in multi-threaded code.
Available Collections
-
ConcurrentBag<T>- Unordered collection -
ConcurrentQueue<T>- FIFO queue -
ConcurrentStack<T>- LIFO stack -
ConcurrentDictionary<TKey, TValue>- Thread-safe dictionary -
BlockingCollection<T>- Blocking producer-consumer
ConcurrentBag<int> bag = new ConcurrentBag<int>();
Parallel.For(0, 1000, i => {
bag.Add(i); // Safe
});
// ConcurrentDictionary
ConcurrentDictionary<int, string> dict = new();
dict.TryAdd(1, "one");
dict.TryGetValue(1, out string value);
dict.AddOrUpdate(1, "one", (key, old) => "ONE");
Solution 4: SemaphoreSlim
What it is: Limits number of threads accessing a resource.
Use: Rate limiting, connection pooling.
Key Methods
-
Wait()/WaitAsync()- Acquire permit (block if none available) -
Release()- Release permit
SemaphoreSlim semaphore = new SemaphoreSlim(3); // Max 3 concurrent
await semaphore.WaitAsync(); // Acquire
try
{
// Do work (max 3 threads here at once)
}
finally
{
semaphore.Release(); // Always release
}
Solution 5: ReaderWriterLockSlim
What it is: Allows multiple readers or one writer.
Use: When reads are common, writes are rare.
ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim();
// Reading
rwLock.EnterReadLock();
try
{
// Multiple readers can be here simultaneously
}
finally
{
rwLock.ExitReadLock();
}
// Writing
rwLock.EnterWriteLock();
try
{
// Only one writer, blocks all readers
}
finally
{
rwLock.ExitWriteLock();
}
Async Coordination Primitives
SemaphoreSlim (Async)
See above - preferred for async scenarios.
ManualResetEventSlim / AutoResetEvent
What it is: Signal between threads.
Use: Rarely needed with async/await.
ManualResetEventSlim mre = new ManualResetEventSlim(false);
// Thread 1: Wait for signal
Task.Run(() => {
mre.Wait(); // Blocks until signaled
Console.WriteLine("Signaled!");
});
// Thread 2: Send signal
Task.Run(() => {
Thread.Sleep(1000);
mre.Set(); // Signal waiting threads
});
TaskCompletionSource<T>
What it is: Manually control Task completion.
Use: Wrap callbacks or events into Task.
TaskCompletionSource<int> tcs = new TaskCompletionSource<int>();
// Simulate async operation
Task.Run(async () => {
await Task.Delay(1000);
tcs.SetResult(42); // Complete the task
});
// Wait for result
int result = await tcs.Task;
Key Methods
-
SetResult(result)- Complete successfully -
SetException(exception)- Complete with error -
SetCanceled()- Complete as canceled -
TrySetResult()/TrySetException()/TrySetCanceled()- Try variants
Common Patterns & Best Practices
Pattern 1: Async All The Way
// ✅ Good: Async all the way
public async Task<string> ControllerActionAsync()
{
var data = await _service.GetDataAsync();
return await _service.ProcessDataAsync(data);
}
// ❌ Bad: Mixing sync and async
public string ControllerAction()
{
var data = _service.GetDataAsync().Result; // DEADLOCK RISK!
return _service.ProcessDataAsync(data).Result;
}
Pattern 2: Fire and Forget (Careful!)
// ❌ Bad: Exceptions are lost
public void StartWork()
{
_ = DoWorkAsync(); // Fire and forget
}
// ✅ Better: Handle exceptions
public void StartWork()
{
_ = Task.Run(async () => {
try
{
await DoWorkAsync();
}
catch (Exception ex)
{
// Log exception
}
});
}
Pattern 3: Lazy Initialization (Thread-Safe)
private readonly Lazy<ExpensiveObject> _lazyObj =
new Lazy<ExpensiveObject>(() => new ExpensiveObject());
public ExpensiveObject Instance => _lazyObj.Value;
Pattern 4: Async Lazy
private readonly AsyncLazy<ExpensiveObject> _asyncLazy =
new AsyncLazy<ExpensiveObject>(async () =>
{
await Task.Delay(100);
return new ExpensiveObject();
});
public Task<ExpensiveObject> GetInstanceAsync() => _asyncLazy.Value;
// Helper class
public class AsyncLazy<T>
{
private readonly Lazy<Task<T>> _lazy;
public AsyncLazy(Func<Task<T>> factory)
{
_lazy = new Lazy<Task<T>>(() => Task.Run(factory));
}
public Task<T> Value => _lazy.Value;
}
Pattern 5: Progress Reporting
public async Task ProcessDataAsync(IProgress<int> progress)
{
for (int i = 0; i < 100; i++)
{
await Task.Delay(10);
progress?.Report(i + 1); // Report progress
}
}
// Usage
var progress = new Progress<int>(percent => {
Console.WriteLine($"Progress: {percent}%");
});
await ProcessDataAsync(progress);
Pattern 6: Timeout Pattern
public async Task<string> GetDataWithTimeoutAsync(int timeoutMs)
{
using (var cts = new CancellationTokenSource(timeoutMs))
{
try
{
return await GetDataAsync(cts.Token);
}
catch (OperationCanceledException)
{
throw new TimeoutException();
}
}
}
// Or using Task.WhenAny
public async Task<string> GetDataWithTimeoutAsync2(int timeoutMs)
{
var dataTask = GetDataAsync();
var timeoutTask = Task.Delay(timeoutMs);
var completedTask = await Task.WhenAny(dataTask, timeoutTask);
if (completedTask == timeoutTask)
throw new TimeoutException();
return await dataTask;
}
Pattern 7: Retry Logic
public async Task<T> RetryAsync<T>(
Func<Task<T>> operation,
int maxAttempts = 3,
int delayMs = 1000)
{
for (int i = 0; i < maxAttempts; i++)
{
try
{
return await operation();
}
catch (Exception) when (i < maxAttempts - 1)
{
await Task.Delay(delayMs);
}
}
throw new Exception("Max retry attempts reached");
}
// Usage
var result = await RetryAsync(() => DownloadDataAsync());
Real-World Applications
1. Web API Calls (I/O-Bound)
public async Task<string> GetWeatherAsync(string city)
{
using (var client = new HttpClient())
{
string url = $"https://api.weather.com/{city}";
return await client.GetStringAsync(url);
}
}
2. Database Operations (I/O-Bound)
public async Task<List<User>> GetUsersAsync()
{
using (var context = new MyDbContext())
{
return await context.Users.ToListAsync();
}
}
3. File Processing (I/O-Bound)
public async Task<string> ReadFileAsync(string path)
{
using (var reader = new StreamReader(path))
{
return await reader.ReadToEndAsync();
}
}
4. Image Processing (CPU-Bound)
public void ProcessImages(List<string> imagePaths)
{
Parallel.ForEach(imagePaths, imagePath => {
var image = LoadImage(imagePath);
var processed = ApplyFilters(image);
SaveImage(processed);
});
}
5. Data Analysis (CPU-Bound)
public double[] AnalyzeData(double[] data)
{
return data
.AsParallel()
.Select(x => ComplexCalculation(x))
.ToArray();
}
6. Responsive UI
private async void Button_Click(object sender, EventArgs e)
{
// UI remains responsive
var data = await LoadDataAsync();
DisplayData(data);
}
7. Background Worker
public class BackgroundWorker
{
private readonly Timer _timer;
public BackgroundWorker()
{
_timer = new Timer(async _ => await DoWorkAsync(),
null, TimeSpan.Zero, TimeSpan.FromMinutes(5));
}
private async Task DoWorkAsync()
{
// Background work every 5 minutes
}
}
8. Producer-Consumer Pattern
BlockingCollection<string> queue = new BlockingCollection<string>();
// Producer
Task.Run(() => {
for (int i = 0; i < 100; i++)
{
queue.Add($"Item {i}");
Thread.Sleep(10);
}
queue.CompleteAdding();
});
// Consumer
Task.Run(() => {
foreach (var item in queue.GetConsumingEnumerable())
{
Console.WriteLine($"Processing {item}");
}
});
⚠️ Common Pitfalls & How to Avoid
1. Deadlock with .Result or .Wait()
// ❌ DEADLOCK in UI or ASP.NET (legacy)
public void Method()
{
var result = GetDataAsync().Result; // BLOCKS!
}
// ✅ Solution: Use async all the way
public async Task MethodAsync()
{
var result = await GetDataAsync();
}
2. Async Void
// ❌ Bad: Can't catch exceptions
public async void ProcessDataAsync()
{
await Task.Delay(1000);
throw new Exception(); // Lost!
}
// ✅ Good: Return Task
public async Task ProcessDataAsync()
{
await Task.Delay(1000);
throw new Exception(); // Can be caught
}
// ⚠️ Exception: Event handlers (only place for async void)
private async void Button_Click(object sender, EventArgs e)
{
await ProcessDataAsync();
}
3. Not Disposing Resources
// ❌ Bad: Resource leak
var stream = new FileStream("file.txt", FileMode.Open);
await stream.ReadAsync(buffer, 0, buffer.Length);
// ✅ Good: Always dispose
using (var stream = new FileStream("file.txt", FileMode.Open))
{
await stream.ReadAsync(buffer, 0, buffer.Length);
}
// ✅ C# 8.0+: Using declaration
await using var stream = new FileStream("file.txt", FileMode.Open);
await stream.ReadAsync(buffer, 0, buffer.Length);
4. Capturing Loop Variables
// ❌ Bad: All tasks capture same variable
for (int i = 0; i < 10; i++)
{
Task.Run(() => Console.WriteLine(i)); // Prints 10 ten times!
}
// ✅ Good: Capture local copy
for (int i = 0; i < 10; i++)
{
int local = i;
Task.Run(() => Console.WriteLine(local)); // Prints 0-9
}
// ✅ C# 5.0+: foreach is safe
foreach (var item in items)
{
Task.Run(() => Console.WriteLine(item)); // Safe
}
5. Parallel.ForEach on UI Collections
// ❌ Bad: UI collections aren't thread-safe
Parallel.ForEach(listBox.Items, item => {
ProcessItem(item); // CRASH!
});
// ✅ Good: Copy to array first
var items = listBox.Items.Cast<string>().ToArray();
Parallel.ForEach(items, item => {
ProcessItem(item);
});
6. Starting Too Many Tasks
// ❌ Bad: Creates 1 million tasks!
var tasks = Enumerable.Range(0, 1000000)
.Select(i => Task.Run(() => DoWork(i)))
.ToArray();
await Task.WhenAll(tasks);
// ✅ Good: Use Parallel.ForEach (manages thread pool)
Parallel.ForEach(Enumerable.Range(0, 1000000), i => {
DoWork(i);
});
// ✅ Good: Batch tasks with SemaphoreSlim
var semaphore = new SemaphoreSlim(10); // Max 10 concurrent
var tasks = Enumerable.Range(0, 1000000)
.Select(async i => {
await semaphore.WaitAsync();
try
{
await DoWorkAsync(i);
}
finally
{
semaphore.Release();
}
})
.ToArray();
await Task.WhenAll(tasks);
7. Mixing CPU and I/O Work
// ❌ Bad: Using async for CPU-bound work
public async Task<int> CalculateAsync(int n)
{
await Task.Delay(1); // Pointless
return HeavyCalculation(n); // Blocks thread anyway
}
// ✅ Good: Use Task.Run for CPU-bound
public Task<int> CalculateAsync(int n)
{
return Task.Run(() => HeavyCalculation(n));
}
// ❌ Bad: Using Parallel for I/O-bound
Parallel.ForEach(urls, url => {
DownloadFile(url).Wait(); // Wastes threads
});
// ✅ Good: Use async for I/O-bound
var tasks = urls.Select(url => DownloadFileAsync(url));
await Task.WhenAll(tasks);
Decision Tree: What to Use When?
Is the work I/O-bound (network, file, database)?
│
├─ YES: Use async/await
│ ├─ Single operation: await SomeMethodAsync()
│ ├─ Multiple operations (sequential): await one, then await another
│ ├─ Multiple operations (parallel): await Task.WhenAll(tasks)
│ └─ Need cancellation: Pass CancellationToken
│
└─ NO: Is it CPU-bound (computation, processing)?
│
├─ YES: Can the work be parallelized?
│ ├─ YES: Loop over collection?
│ │ ├─ YES: Use Parallel.ForEach or Parallel.For
│ │ └─ NO: Use Parallel.Invoke
│ │
│ └─ NO: Use Task.Run() to offload to thread pool
│
└─ NO: Use synchronous code (normal method)
Special cases:
- Event handling: Event-driven programming
- Rate limiting: SemaphoreSlim
- Shared state: lock, Interlocked, or Concurrent collections
- Background work: Timer + async Task
- Producer-Consumer: BlockingCollection
Performance Comparison
Task Creation Cost
| Method | Relative Cost | Use Case |
|---|---|---|
await existing Task |
1x (baseline) | Always prefer |
Task.CompletedTask |
~1x | Synchronous async methods |
Task.FromResult() |
~5x | Return cached result |
new Task() + Start()
|
~50x | Legacy, avoid |
new Thread() |
~500x | Legacy, avoid |
Async vs Sync File I/O (1000 files)
- Synchronous: ~30 seconds (one at a time)
- Asynchronous: ~3 seconds (parallel I/O operations)
Parallel.ForEach vs Sequential (CPU-bound, 4 cores)
- Sequential: 10 seconds
- Parallel: ~2.5 seconds (4x faster)
Async Streams (C# 8.0+)
What it is: Asynchronously iterate over data as it arrives.
Use: Large datasets, real-time data, pagination.
IAsyncEnumerable<T>
// Producer
public async IAsyncEnumerable<int> GetNumbersAsync()
{
for (int i = 0; i < 10; i++)
{
await Task.Delay(100); // Simulate async work
yield return i;
}
}
// Consumer
await foreach (var number in GetNumbersAsync())
{
Console.WriteLine(number); // Process as they arrive
}
// With cancellation
public async IAsyncEnumerable<int> GetNumbersAsync(
[EnumeratorCancellation] CancellationToken token = default)
{
for (int i = 0; i < 10; i++)
{
token.ThrowIfCancellationRequested();
await Task.Delay(100, token);
yield return i;
}
}
Real-World Example: Paginated API
public async IAsyncEnumerable<User> GetAllUsersAsync()
{
int page = 1;
while (true)
{
var users = await GetUserPageAsync(page);
if (users.Count == 0) break;
foreach (var user in users)
yield return user;
page++;
}
}
// Usage
await foreach (var user in GetAllUsersAsync())
{
Console.WriteLine(user.Name); // Process one at a time
}
ValueTask<T> (Performance Optimization)
What it is: Struct-based Task alternative to avoid allocations.
Use: High-performance scenarios where result is often available synchronously.
Caution: More restrictions than Task.
When to Use
// ❌ Use Task<T>: Usually async
public async Task<string> DownloadAsync(string url)
{
return await httpClient.GetStringAsync(url);
}
// ✅ Use ValueTask<T>: Often cached/synchronous
public ValueTask<User> GetUserAsync(int id)
{
if (_cache.TryGetValue(id, out var user))
return new ValueTask<User>(user); // No allocation
return new ValueTask<User>(LoadUserAsync(id));
}
Restrictions
- Can only await once
- Cannot use
.Resultor.Wait() - Cannot await simultaneously from multiple threads
- If in doubt, use
Task<T>
ConfigureAwait Deep Dive
What It Does
// Default: ConfigureAwait(true)
await SomeMethodAsync();
// After await, execution resumes on captured context (UI thread)
// ConfigureAwait(false)
await SomeMethodAsync().ConfigureAwait(false);
// After await, execution resumes on any available thread
When to Use ConfigureAwait(false)
- ✅ Library code (never touches UI)
- ✅ ASP.NET Core (no synchronization context)
- ✅ Background services
- ✅ Performance-critical code
When NOT to Use ConfigureAwait(false)
- ❌ UI code (WPF, WinForms, Xamarin)
- ❌ Code that accesses UI elements after await
- ❌ ASP.NET Framework (before Core)
Example
// Library code - always use ConfigureAwait(false)
public async Task<string> GetDataAsync()
{
var response = await httpClient
.GetAsync(url)
.ConfigureAwait(false);
var content = await response.Content
.ReadAsStringAsync()
.ConfigureAwait(false);
return content;
}
// UI code - don't use ConfigureAwait(false)
private async void Button_Click(object sender, EventArgs e)
{
var data = await GetDataAsync(); // Default: stays on UI thread
textBox.Text = data; // Safe to access UI
}
Advanced: Custom Awaiter
What it is: Make any type awaitable.
Use: Rare, but shows how await works under the hood.
public static class IntExtensions
{
public static TaskAwaiter GetAwaiter(this int milliseconds)
{
return Task.Delay(milliseconds).GetAwaiter();
}
}
// Usage
await 1000; // Wait 1 second!
Channels (System.Threading.Channels)
What it is: Modern producer-consumer queue for async scenarios.
Use: Better alternative to BlockingCollection for async code.
Channel Types
-
Channel.CreateUnbounded<T>()- No capacity limit -
Channel.CreateBounded<T>(capacity)- Limited capacity
Example
var channel = Channel.CreateUnbounded<int>();
// Producer
var writer = channel.Writer;
_ = Task.Run(async () => {
for (int i = 0; i < 100; i++)
{
await writer.WriteAsync(i);
await Task.Delay(10);
}
writer.Complete();
});
// Consumer
var reader = channel.Reader;
await foreach (var item in reader.ReadAllAsync())
{
Console.WriteLine(item);
}
// Or manual consumption
while (await reader.WaitToReadAsync())
{
while (reader.TryRead(out var item))
{
Console.WriteLine(item);
}
}
Testing Async Code
Basic Test
[Test]
public async Task TestAsyncMethod()
{
var result = await GetDataAsync();
Assert.AreEqual("expected", result);
}
Testing with Timeout
[Test, Timeout(5000)] // 5 second timeout
public async Task TestWithTimeout()
{
await LongRunningOperationAsync();
}
Testing Cancellation
[Test]
public async Task TestCancellation()
{
var cts = new CancellationTokenSource();
var task = LongRunningOperationAsync(cts.Token);
cts.Cancel();
await Assert.ThrowsAsync<OperationCanceledException>(
async () => await task);
}
Testing Parallel Code
[Test]
public void TestThreadSafety()
{
int counter = 0;
object lockObj = new object();
Parallel.For(0, 1000, i => {
lock (lockObj)
{
counter++;
}
});
Assert.AreEqual(1000, counter);
}
Quick Reference Summary
Legacy (Pre-.NET 4.5) ❌
-
Thread- Manual thread management -
BackgroundWorker- UI threading helper -
ThreadPool.QueueUserWorkItem- Queue work items -
APM(Begin/End) - IAsyncResult pattern -
EAP(Events) - MethodAsync + Completed event
Modern (.NET 4.5+) ✅
-
async/await- Write async code naturally -
Task/Task<T>- Represent async operations -
Task.Run()- Offload CPU work -
Task.WhenAll/WhenAny- Coordinate tasks -
CancellationToken- Cooperative cancellation -
Parallel.ForEach/For- Data parallelism -
PLINQ- Parallel LINQ -
SemaphoreSlim- Async coordination -
IProgress<T>- Progress reporting
Cutting Edge (.NET Core 3.0+) 🚀
-
IAsyncEnumerable<T>- Async streams -
Channels- Modern producer-consumer -
ValueTask<T>- Zero-allocation async -
IAsyncDisposable- Async resource disposal
Cheat Sheet: Syntax Quick Look
// Async method signatures
public async Task MethodAsync() { }
public async Task<int> MethodAsync() { }
public async ValueTask<int> MethodAsync() { }
public async IAsyncEnumerable<int> MethodAsync() { }
// Awaiting
var result = await SomeMethodAsync();
await Task.Delay(1000);
await Task.WhenAll(task1, task2);
var first = await Task.WhenAny(task1, task2);
// Fire and forget (avoid!)
_ = SomeMethodAsync();
// Parallel loops
Parallel.For(0, 100, i => { });
Parallel.ForEach(items, item => { });
Parallel.Invoke(() => Method1(), () => Method2());
// PLINQ
var results = items.AsParallel()
.Where(x => x.IsValid)
.Select(x => Process(x))
.ToList();
// Cancellation
var cts = new CancellationTokenSource();
cts.CancelAfter(5000);
await MethodAsync(cts.Token);
// Thread safety
lock (lockObj) { counter++; }
Interlocked.Increment(ref counter);
var dict = new ConcurrentDictionary<int, string>();
// Semaphore
await semaphore.WaitAsync();
try { } finally { semaphore.Release(); }
// Async streams
await foreach (var item in GetItemsAsync()) { }
// Progress
var progress = new Progress<int>(p => Console.WriteLine(p));
await ProcessAsync(progress);
Mental Model: The Right Tool for the Job
| Scenario | Use This | Why |
|---|---|---|
| Web API call | async/await |
I/O-bound, non-blocking |
| Database query | async/await |
I/O-bound, non-blocking |
| File reading | async/await |
I/O-bound, non-blocking |
| Image processing | Parallel.ForEach |
CPU-bound, data parallel |
| Heavy calculation | Task.Run() |
CPU-bound, offload from UI |
| Data analysis | PLINQ |
CPU-bound, query processing |
| Rate limiting | SemaphoreSlim |
Control concurrency |
| Shared counter | Interlocked |
Simple atomic operations |
| Shared dictionary | ConcurrentDictionary |
Complex shared state |
| Background timer |
Timer + async Task
|
Periodic work |
| Producer-consumer | Channel |
Modern async queue |
| Event handling |
event + async void
|
UI interactions |
| Streaming data | IAsyncEnumerable |
Process as data arrives |
Final Tips
- Start with async/await - It's the right default for most scenarios
- Use Task.Run only for CPU-bound work - Don't wrap I/O operations
-
Async all the way - Don't block on async code with
.Resultor.Wait() - Avoid async void - Except for event handlers
-
Always dispose - Use
usingfor disposable resources -
Configure await in libraries - Use
.ConfigureAwait(false)in library code - Cancellation tokens everywhere - Support cancellation in async methods
- Handle exceptions - Always try-catch in fire-and-forget scenarios
- Test thread safety - Use concurrent collections or locks
- Profile before parallel - Parallelism has overhead; measure actual gains
Remember: Async doesn't mean parallel, and parallel doesn't mean async!
- Async = Non-blocking (I/O-bound)
- Parallel = Simultaneous execution (CPU-bound)
Top comments (0)