ConcurrencyC#verifiedVerified
Thread Pool Pattern in C#
Maintain a fixed set of reusable worker threads that pick up tasks from a queue, avoiding the overhead of spawning a new thread per task.
How to Implement the Thread Pool Pattern in C#
1Step 1: Define a simple custom thread pool
public sealed class SimpleThreadPool : IDisposable
{
private readonly Thread[] _workers;
private readonly Channel<Action> _taskQueue;
private bool _disposed;
public SimpleThreadPool(int workerCount)
{
_taskQueue = Channel.CreateUnbounded<Action>();
_workers = new Thread[workerCount];2Step 2: Start worker threads
for (var i = 0; i < workerCount; i++)
{
_workers[i] = new Thread(WorkerLoop)
{
IsBackground = true,
Name = $"Pool-Worker-{i}"
};
_workers[i].Start();
}
}3Step 3: Enqueue work items
public void Submit(Action task) =>
_taskQueue.Writer.TryWrite(task);
private void WorkerLoop()
{
foreach (var task in _taskQueue.Reader.ReadAllAsync()
.ToBlockingEnumerable())
{
try { task(); }
catch (Exception ex)
{
Console.WriteLine($"Task error: {ex.Message}");
}
}
}
public void Dispose()
{
if (_disposed) return;
_disposed = true;
_taskQueue.Writer.Complete();
foreach (var w in _workers) w.Join(1000);
}
}using System.Collections.Concurrent;
using System.Threading.Channels;
using Microsoft.Extensions.Logging;
// [step] Define work item with priority and result tracking
public record PoolWorkItem(
string Id, Func<CancellationToken, Task> Work,
TaskCompletionSource<bool> Completion,
Priority Priority = Priority.Normal);
public enum Priority { Low, Normal, High }
public record PoolStatistics(
int ActiveWorkers, int QueuedTasks,
long CompletedTasks, long FailedTasks,
TimeSpan AverageLatency);
// [step] Production thread pool with dynamic scaling and metrics
public sealed class ManagedThreadPool : IAsyncDisposable
{
private readonly Channel<PoolWorkItem> _queue;
private readonly List<Task> _workers = [];
private readonly CancellationTokenSource _cts = new();
private readonly ILogger<ManagedThreadPool> _logger;
private readonly int _minWorkers;
private readonly int _maxWorkers;
private int _activeWorkers;
private long _completed;
private long _failed;
private long _totalLatencyMs;
public ManagedThreadPool(
int minWorkers, int maxWorkers, int queueCapacity,
ILogger<ManagedThreadPool> logger)
{
_minWorkers = minWorkers;
_maxWorkers = maxWorkers;
_logger = logger;
_queue = Channel.CreateBounded<PoolWorkItem>(
new BoundedChannelOptions(queueCapacity)
{
FullMode = BoundedChannelFullMode.Wait
});
// [step] Start minimum number of workers
for (var i = 0; i < minWorkers; i++)
SpawnWorker(i);
_logger.LogInformation(
"Thread pool started: {Min}-{Max} workers, capacity {Cap}",
minWorkers, maxWorkers, queueCapacity);
}
// [step] Submit work with result tracking
public async Task<bool> SubmitAsync(
Func<CancellationToken, Task> work,
Priority priority = Priority.Normal,
CancellationToken ct = default)
{
var tcs = new TaskCompletionSource<bool>();
var item = new PoolWorkItem(
Guid.NewGuid().ToString(), work, tcs, priority);
await _queue.Writer.WriteAsync(item, ct);
_logger.LogDebug("Queued work item {Id}", item.Id);
// Auto-scale if needed
if (_queue.Reader.Count > _workers.Count * 2
&& _workers.Count < _maxWorkers)
{
SpawnWorker(_workers.Count);
}
return await tcs.Task;
}
private void SpawnWorker(int index)
{
var worker = Task.Run(async () =>
{
_logger.LogDebug("Worker-{Index} started", index);
await foreach (var item in
_queue.Reader.ReadAllAsync(_cts.Token))
{
Interlocked.Increment(ref _activeWorkers);
var sw = System.Diagnostics.Stopwatch.StartNew();
try
{
await item.Work(_cts.Token);
Interlocked.Increment(ref _completed);
item.Completion.SetResult(true);
}
catch (OperationCanceledException)
{
item.Completion.SetCanceled();
}
catch (Exception ex)
{
Interlocked.Increment(ref _failed);
item.Completion.SetException(ex);
_logger.LogError(ex, "Work item {Id} failed", item.Id);
}
finally
{
Interlocked.Decrement(ref _activeWorkers);
Interlocked.Add(ref _totalLatencyMs,
sw.ElapsedMilliseconds);
}
}
}, _cts.Token);
_workers.Add(worker);
}
// [step] Get pool statistics
public PoolStatistics GetStatistics()
{
var completed = Interlocked.Read(ref _completed);
var totalMs = Interlocked.Read(ref _totalLatencyMs);
return new PoolStatistics(
_activeWorkers,
_queue.Reader.Count,
completed,
Interlocked.Read(ref _failed),
completed > 0
? TimeSpan.FromMilliseconds((double)totalMs / completed)
: TimeSpan.Zero);
}
public async ValueTask DisposeAsync()
{
_queue.Writer.Complete();
await _cts.CancelAsync();
await Task.WhenAll(_workers);
_cts.Dispose();
}
}Thread Pool Pattern Architecture
hourglass_empty
Rendering diagram...
lightbulb
Thread Pool Pattern in the Real World
“A hotel concierge desk staffed by three concierges represents the thread pool. No matter how many guests check in, only three requests are handled simultaneously. Other guests wait in the lobby queue. When a concierge finishes, they immediately assist the next waiting guest — the staff are never created or dismissed per guest, they simply stay on duty.”