15 minlesson

The Thread Pool

The Thread Pool

Creating threads is expensive. The Thread Pool solves this by maintaining a pool of reusable threads, significantly improving performance for short-lived operations.

Why Use a Thread Pool?

Direct ThreadsThread Pool
~1ms to createAlready created
~1MB stack eachShared resources
Must manage lifecycleAutomatic management
Can exhaust resourcesAutomatically throttled
1Without Pool: With Pool:
2┌──────────────┐ ┌──────────────┐
3│ Create Thread│ │ Request from │
4│ (~1ms) │ │ Pool │
5└──────┬───────┘ └──────┬───────┘
6 │ │
7 ▼ ▼
8┌──────────────┐ ┌──────────────┐
9│ Do Work │ │ Do Work │
10└──────┬───────┘ └──────┬───────┘
11 │ │
12 ▼ ▼
13┌──────────────┐ ┌──────────────┐
14│Destroy Thread│ │Return to Pool│
15└──────────────┘ └──────────────┘

Using the Thread Pool

QueueUserWorkItem

The simplest way to use the thread pool:

csharp
1// Queue work item
2ThreadPool.QueueUserWorkItem(_ =>
3{
4 Console.WriteLine($"Running on pool thread: {Thread.CurrentThread.ManagedThreadId}");
5 Console.WriteLine($"Is pool thread: {Thread.CurrentThread.IsThreadPoolThread}");
6});
7
8// With state parameter
9ThreadPool.QueueUserWorkItem(state =>
10{
11 string message = (string)state!;
12 Console.WriteLine(message);
13}, "Hello from thread pool!");

Thread Pool Information

csharp
1// Get pool thread counts
2ThreadPool.GetMinThreads(out int minWorker, out int minIO);
3ThreadPool.GetMaxThreads(out int maxWorker, out int maxIO);
4ThreadPool.GetAvailableThreads(out int availWorker, out int availIO);
5
6Console.WriteLine($"Worker threads - Min: {minWorker}, Max: {maxWorker}, Available: {availWorker}");
7Console.WriteLine($"I/O threads - Min: {minIO}, Max: {maxIO}, Available: {availIO}");

Configuring the Pool

csharp
1// Set minimum threads (useful for burst scenarios)
2ThreadPool.SetMinThreads(workerThreads: 10, completionPortThreads: 10);
3
4// Set maximum threads
5ThreadPool.SetMaxThreads(workerThreads: 100, completionPortThreads: 100);

Worker vs I/O Threads

The thread pool has two types of threads:

Worker ThreadsI/O Completion Threads
CPU-bound operationsAsync I/O callbacks
Computation, processingFile, network, database
QueueUserWorkItemAutomatic for async I/O

Thread Pool Behavior

Thread Injection

When all pool threads are busy, the pool gradually adds new threads:

csharp
1// Simulate thread starvation
2for (int i = 0; i < 100; i++)
3{
4 ThreadPool.QueueUserWorkItem(_ =>
5 {
6 Console.WriteLine($"Task on thread {Thread.CurrentThread.ManagedThreadId}");
7 Thread.Sleep(10000); // Block for 10 seconds
8 });
9}
10
11// Watch thread count increase over time
12for (int i = 0; i < 10; i++)
13{
14 ThreadPool.GetAvailableThreads(out int avail, out _);
15 ThreadPool.GetMaxThreads(out int max, out _);
16 Console.WriteLine($"Active pool threads: {max - avail}");
17 Thread.Sleep(1000);
18}

Note: Thread injection is slow (~500ms per thread) to prevent oversubscription.

Limitations of QueueUserWorkItem

The thread pool's basic API has limitations:

csharp
1// Cannot easily wait for completion
2ThreadPool.QueueUserWorkItem(_ => DoWork());
3// How do we know when DoWork is done?
4
5// Cannot get a return value
6// How do we get the result?
7
8// Limited error handling
9// Exceptions crash the app!

This is why Tasks were introduced - they build on the thread pool but provide:

  • Return values
  • Exception handling
  • Continuation/chaining
  • Cancellation
  • Progress reporting

Thread Pool Best Practices

Do

  • Use for short-lived operations
  • Let the pool manage thread count
  • Use Tasks for most scenarios (they use the pool internally)

Don't

  • Block pool threads for long periods
  • Create threads manually for short work
  • Exhaust the pool with too many long-running operations
csharp
1// BAD: Blocking pool threads
2ThreadPool.QueueUserWorkItem(_ =>
3{
4 Thread.Sleep(60000); // Blocks a pool thread for 1 minute!
5});
6
7// BETTER: Use dedicated thread for long-running work
8Thread longRunning = new Thread(LongRunningWork);
9longRunning.IsBackground = true;
10longRunning.Start();

Key Takeaways

  • Thread pool reuses threads to avoid creation overhead
  • ThreadPool.QueueUserWorkItem() is the basic API
  • Pool automatically grows and shrinks based on demand
  • Thread injection is slow by design
  • Don't block pool threads for extended periods
  • Tasks are the modern way to use the pool (coming next!)