63

I know that Thread.Sleep blocks a thread.

But does Task.Delay also block? Or is it just like Timer which uses one thread for all callbacks (when not overlapping)?

(this question doesn't cover the differences)

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
Royi Namir
  • 144,742
  • 138
  • 468
  • 792
  • 2
    Here's a little [wiki](http://social.technet.microsoft.com/wiki/contents/articles/21177.visual-c-thread-sleep-vs-task-delay.aspx) with a demo highlighting some blocking differences between the two. – jxramos Apr 01 '15 at 23:16
  • Possible duplicate of [Thread.Sleep(2500) vs. Task.Delay(2500).Wait()](http://stackoverflow.com/questions/34052381/thread-sleep2500-vs-task-delay2500-wait) – Teoman shipahi Mar 08 '17 at 19:10
  • Does this answer your question? [When to use Task.Delay, when to use Thread.Sleep?](https://stackoverflow.com/questions/20082221/when-to-use-task-delay-when-to-use-thread-sleep) – T.Todua Jun 24 '22 at 15:46

2 Answers2

59

The documentation on MSDN is disappointing, but decompiling Task.Delay using Reflector gives more information:

public static Task Delay(int millisecondsDelay, CancellationToken cancellationToken)
{
    if (millisecondsDelay < -1)
    {
        throw new ArgumentOutOfRangeException("millisecondsDelay", Environment.GetResourceString("Task_Delay_InvalidMillisecondsDelay"));
    }
    if (cancellationToken.IsCancellationRequested)
    {
        return FromCancellation(cancellationToken);
    }
    if (millisecondsDelay == 0)
    {
        return CompletedTask;
    }
    DelayPromise state = new DelayPromise(cancellationToken);
    if (cancellationToken.CanBeCanceled)
    {
        state.Registration = cancellationToken.InternalRegisterWithoutEC(delegate (object state) {
            ((DelayPromise) state).Complete();
        }, state);
    }
    if (millisecondsDelay != -1)
    {
        state.Timer = new Timer(delegate (object state) {
            ((DelayPromise) state).Complete();
        }, state, millisecondsDelay, -1);
        state.Timer.KeepRootedWhileScheduled();
    }
    return state;
}

Basically, this method is just a timer wrapped inside of a task. So yes, you can say it's just like timer.

Kevin Gosse
  • 38,392
  • 3
  • 78
  • 94
  • 1
    Does it blocks or not ? – Royi Namir Apr 02 '14 at 04:46
  • 1
    @RoyiNamir I don't understand your question. It's a task, whether it blocks or not all depends on the way you use it. – Kevin Gosse Apr 05 '14 at 11:20
  • 18
    Think of "await Task.Delay(1000)" as the async version of Thread.Sleep(1000), as it doesn't block. I use it in GUI tools when I need to wait but don't want to block the UI or eat up threads. – hko Jun 06 '14 at 07:07
4

No, the Task.Delay doesn't block the current thread. It can be used to block it, but it doesn't do it by itself, and it's rarely used as a synchronous blocker in practice. All it actually does is to return a Task that will complete after the specified amount of time:

Task task = Task.Delay(1000); // The task will complete after 1,000 milliseconds.

Typically this task is then waited asynchronously with the await keyword, inside an async method:

await task; // Suspends the async method, but doesn't block the thread.

The await keyword suspends the current execution flow (async method) until the awaitable completes. No thread is blocked while the execution flow is suspended.

It is also possible to block the current thread until the task completes, by using the synchronous Wait method.

task.Wait(); // Blocks the thread.

If you would like to see an experimental demonstration that the await Task.Delay() doesn't block a thread, here is one. The program below creates a huge number of tasks, where each task awaits a internally a Task.Delay(1000). Then the number of threads used by the current process is printed in the console, and finally all of the tasks are awaited:

Task[] tasks = Enumerable.Range(1, 100_000).Select(async _ =>
{
    await Task.Delay(1000);
}).ToArray();
Console.WriteLine($"Tasks: {tasks.Count(t => t.IsCompleted):#,0} / {tasks.Length:#,0}");
Thread.Sleep(500);
Console.WriteLine($"Threads.Count: {Process.GetCurrentProcess().Threads.Count:#,0}");
await Task.WhenAll(tasks);
Console.WriteLine($"Tasks: {tasks.Count(t => t.IsCompleted):#,0} / {tasks.Length:#,0}");

Output:

Tasks: 0 / 100,000
Threads.Count: 9
Tasks: 100,000 / 100,000

Live demo.

The program completes after just 1 second, and reports that during its peak it used a total of 9 threads. If each of the 100,000 tasks blocked a thread, we would expect to see 100,000 threads used at that point. Apparently this didn't happen.

Theodor Zoulias
  • 34,835
  • 7
  • 69
  • 104
  • Just a comment, but the thread count after the WhenAll is 14. – Josh Mouch Jun 24 '22 at 14:19
  • @JoshMouch I guess that when the 100,000 tasks completed, each one used briefly a thread from the `ThreadPool`. So the `ThreadPool` created a few threads to satisfy the demand, probably 4 threads. This could explain the increase from 9 to 13 total threads. I have no theory about the 14th thread. – Theodor Zoulias Aug 26 '22 at 22:55