The thread pool
Starting an OS thread is expensive: it allocates a stack and a kernel object, and many threads on few cores waste time on context switches. A runtime avoids that with a pool of worker threads that are created once and reused. Work goes into a queue, and a free worker takes the next item.
What runs on it
.NET’s ThreadPool runs Task work items, such as Task.Run, and the continuations that resume a method after an await. Parallel loops and PLINQ use it too.
Sizing
The pool starts with a minimum of about one thread per core. When work is queued and every thread is busy, it adds threads, but slowly: roughly one every 500 ms once the minimum is reached. The maximum is very large, about 32,767 by default. SetMinThreads and SetMaxThreads change the limits, and the defaults are usually right.
Starvation
A pool thread blocked by Thread.Sleep or .Result is out of service while the queue keeps growing. New threads arrive slowly, so the queued work waits. This is thread pool starvationLivelock and starvationThreads that keep running without progress, and threads that never get a turn.Opens in a new tab, and it shows up as sudden latency under load (see Thread.Sleep versus Task.DelayThread.Sleep versus Task.DelaySleep blocks the thread. Delay frees it.Opens in a new tab).
Where it matters
A small pool can serve a large number of requests as long as threads are not held while waiting, which async I/O ensures (see how many threads handle one requestHow many threads handle one request?One at a time, but not always the same one.Opens in a new tab).