| Method |
Use For |
Thread Source |
new Thread() |
Full control, long-blocking |
New OS thread |
ThreadPool.QueueUserWorkItem |
Fire-and-forget |
ThreadPool |
Task.Run |
Modern async code |
ThreadPool |
TaskFactory.StartNew |
Custom scheduler, LongRunning |
Configurable |
Parallel.For/ForEach |
Data parallelism |
ThreadPool |
Task.Run(() => DoWork());
await Task.Run(async () => await DoWorkAsync());
Task.Factory.StartNew(() => BlockingLoop(), TaskCreationOptions.LongRunning);
Parallel.ForEach(items, item => Process(item));
await Parallel.ForEachAsync(items, async (item, ct) => await ProcessAsync(item, ct));
| Primitive |
Async |
Best For |
lock |
No |
Short sync sections |
SemaphoreSlim |
Yes |
Async, throttling |
SpinLock |
No |
Very short locks |
ReaderWriterLockSlim |
No |
Read-heavy workloads |
private readonly object _lock = new();
lock (_lock) { DoWork(); }
private readonly SemaphoreSlim _semaphore = new(1, 1);
await _semaphore.WaitAsync();
try { await DoWorkAsync(); }
finally { _semaphore.Release(); }
public sealed class AsyncLock
{
private readonly SemaphoreSlim _semaphore = new(1, 1);
public async Task<IDisposable> LockAsync()
{
await _semaphore.WaitAsync();
return new Releaser(_semaphore);
}
private class Releaser : IDisposable
{
private readonly SemaphoreSlim _semaphore;
public Releaser(SemaphoreSlim s) => _semaphore = s;
public void Dispose() => _semaphore.Release();
}
}
using (await _lock.LockAsync())
{
await DoWorkAsync();
}
| Collection |
Lock-Free |
Best For |
ConcurrentDictionary<K,V> |
Partial |
Key-value lookups |
ConcurrentQueue<T> |
Yes |
FIFO |
ConcurrentStack<T> |
Yes |
LIFO |
ConcurrentBag<T> |
Per-thread |
Unordered add/take |
Channel<T> |
Partial |
Async producer-consumer |
var value = dict.GetOrAdd(key, k => ComputeValue(k));
var lazyDict = new ConcurrentDictionary<string, Lazy<int>>();
var value = lazyDict.GetOrAdd(key, k => new Lazy<int>(() => ComputeValue(k))).Value;
dict.AddOrUpdate(key, 1, (k, old) => old + 1);
var channel = Channel.CreateBounded<int>(new BoundedChannelOptions(100)
{
FullMode = BoundedChannelFullMode.Wait
});
await channel.Writer.WriteAsync(item);
channel.Writer.Complete();
await foreach (var item in channel.Reader.ReadAllAsync())
{
await ProcessAsync(item);
}
| Mode |
Behavior |
Wait |
WriteAsync blocks until space |
DropOldest |
Removes oldest item |
DropNewest |
Drops item being written |
DropWrite |
Returns false, no exception |
private volatile bool _flag;
_flag = true;
private int _counter;
Interlocked.Increment(ref _counter);
Interlocked.Add(ref _counter, 10);
Interlocked.CompareExchange(ref _counter, newValue, expected);
- volatile: Simple flags (bool), status indicators
- Interlocked: Counters, any read-modify-write
- lock: Multiple related updates
public async IAsyncEnumerable<int> GenerateAsync(
[EnumeratorCancellation] CancellationToken ct = default)
{
for (int i = 0; i < 100; i++)
{
await Task.Delay(100, ct);
yield return i;
}
}
await foreach (var item in GenerateAsync(cancellationToken))
{
Process(item);
}
| Aspect |
ValueTask |
Task |
| Allocation |
None if sync |
Always |
| Await count |
Once only! |
Multiple OK |
| Store for later |
No |
Yes |
| Use when |
Often sync (cache) |
Always async |
public ValueTask<int> GetAsync()
{
return _hasCache
? new ValueTask<int>(_cached)
: new ValueTask<int>(LoadAsync());
}
var vt = GetAsync();
await vt;
await vt;
var results = await Task.WhenAll(task1, task2, task3);
var winner = await Task.WhenAny(task1, task2, task3);
var result = await winner;
var work = DoWorkAsync();
var timeout = Task.Delay(TimeSpan.FromSeconds(30));
if (await Task.WhenAny(work, timeout) == timeout)
throw new TimeoutException();
var semaphore = new SemaphoreSlim(maxConcurrency);
var tasks = items.Select(async item =>
{
await semaphore.WaitAsync();
try { return await ProcessAsync(item); }
finally { semaphore.Release(); }
});
var results = await Task.WhenAll(tasks);
var query = list.Where(x => x > 10);
var results = list.Where(x => x > 10).ToList();
ILookup<int, Item> lookup = items.ToLookup(i => i.Category);
var cat1 = lookup[1];
var cat99 = lookup[99];
IEnumerable<IGrouping<int, Item>> groups = items.GroupBy(i => i.Category);
Q: Task.Run vs TaskFactory.StartNew?
- Task.Run: Always ThreadPool, unwraps async lambdas
- StartNew: Configurable scheduler, may need Unwrap()
Q: Why canβt use lock with async?
- lock is thread-affine, await may resume on different thread
Q: volatile vs Interlocked?
- volatile: Visibility only (simple flags)
- Interlocked: Atomic operations (counters)
Q: What are Channels?
- Async-first producer-consumer queues
- Better than BlockingCollection for async code
Q: Deferred execution gotchas?
- Multiple enumeration re-executes query
- Source changes affect results before enumeration
| Keyword |
Must Init |
Can Modify |
Use For |
| (none) |
Yes |
Copy |
Default |
ref |
Yes |
Yes |
Modify callerβs var |
out |
No |
Must assign |
Multiple returns |
in |
Yes |
No |
Large struct perf |
await task.ConfigureAwait(false);
await task;
public async Task DoWorkAsync(CancellationToken ct = default)
{
while (!ct.IsCancellationRequested)
{
ct.ThrowIfCancellationRequested();
await ProcessAsync(ct);
}
}