Thread.Sleep versus Task.Delay

2 min read

Thread.Sleep blocks the current thread: the OS takes it off the CPU and does not schedule it for at least the given time. await Task.Delay registers a timer and frees the thread.

Sleep

It is a thin wrapper over an OS call (SleepEx, nanosleep). The thread moves to a waiting state and the core goes to another thread. When the timer fires, the thread is ready but runs only when scheduled, so the delay is at least the requested time (about 15 ms granularity historically on Windows). A sleeping thread keeps its stack, about 1 MB reserved on Windows. On a pool threadThe thread poolA set of reusable worker threads that take jobs from a queue.Opens in a new tab, 50 requests calling Sleep(1000) tie up 50 pool threads for a second with no CPU work, which leads to thread pool starvationLivelock and starvationThreads that keep running without progress, and threads that never get a turn.Opens in a new tab.

Delay

The runtime registers a timer and returns control to the caller, so the thread goes back to the pool. When the timer fires, the continuation is scheduled on a pool thread (see how await worksHow await works under the hoodThe compiler turns an async method into a state machine, and the thread is freed while it waits.Opens in a new tab).

Spin-waiting

Waiting by looping on a flag keeps the thread running. It checks the flag over and over, using 100% of a core without getting anything done. Sleeping parks the thread instead, which leaves the core free for other work.

C#
while (!isDone) { } // spin: burns 100% of a core doing nothing

Spinning only makes sense for waits of a few nanoseconds, which is how low-level locks use it. For anything longer, sleep or await.

Sleep(0) and Sleep(1)

A spin loop sometimes calls Thread.Sleep(0) or Thread.Sleep(1) so other threads get a turn. Sleep(0) gives up the rest of the thread’s time slice (the few milliseconds the OS allows before switching). The core goes to another ready thread of equal or higher priority, and if none is waiting, the thread carries on at once. Sleep(1) really sleeps, for about 1 ms or more, so any ready thread can run. Outside custom locks, neither is needed.

Where it matters

It is the same blocking versus non-blocking split as a callback blocking Node’s event loopNode's event loop compared with the .NET thread poolNode runs your code on one thread. .NET runs it on many.Opens in a new tab.