10

I'm confused why Task.Delay().Wait() takes 4x more time, then Thread.Sleep()?

E.g. task-00 was running on only thread 9 and took 2193ms? I'm aware, that sync wait is bad in tasks, because the whole thread being blocked. It is just for test.

Simple test in console application:

bool flag = true;
var sw = Stopwatch.StartNew();
for (int i = 0; i < 10; i++)
{
    var cntr = i;
    {
        var start = sw.ElapsedMilliseconds;
        var wait = flag ? 100 : 300;
        flag = !flag;

        Task.Run(() =>
        {
            Console.WriteLine($"task-{cntr.ToString("00")} \t ThrID: {Thread.CurrentThread.ManagedThreadId.ToString("00")},\t Wait={wait}ms, \t START: {start}ms");                     
            //Thread.Sleep(wait);
            Task.Delay(wait).Wait();
            Console.WriteLine($"task-{cntr.ToString("00")} \t ThrID: {Thread.CurrentThread.ManagedThreadId.ToString("00")},\t Wait={wait}ms, \t END: {sw.ElapsedMilliseconds}ms");
            ;
        });
    }
}
Console.ReadKey();
return;

With Task.Delay().Wait():
task-03 ThrID: 05, Wait=300ms, START: 184ms
task-04 ThrID: 07, Wait=100ms, START: 184ms
task-00 ThrID: 09, Wait=100ms, START: 0ms
task-06 ThrID: 04, Wait=100ms, START: 185ms
task-01 ThrID: 08, Wait=300ms, START: 183ms
task-05 ThrID: 03, Wait=300ms, START: 185ms
task-02 ThrID: 06, Wait=100ms, START: 184ms
task-07 ThrID: 10, Wait=300ms, START: 209ms
task-07 ThrID: 10, Wait=300ms, END: 1189ms
task-08 ThrID: 12, Wait=100ms, START: 226ms
task-09 ThrID: 10, Wait=300ms, START: 226ms
task-09 ThrID: 10, Wait=300ms, END: 2192ms
task-06 ThrID: 04, Wait=100ms, END: 2193ms
task-08 ThrID: 12, Wait=100ms, END: 2194ms
task-05 ThrID: 03, Wait=300ms, END: 2193ms
task-03 ThrID: 05, Wait=300ms, END: 2193ms
task-00 ThrID: 09, Wait=100ms, END: 2193ms
task-02 ThrID: 06, Wait=100ms, END: 2193ms
task-04 ThrID: 07, Wait=100ms, END: 2193ms
task-01 ThrID: 08, Wait=300ms, END: 2193ms

With Thread.Sleep():
task-00 ThrID: 03, Wait=100ms, START: 0ms
task-03 ThrID: 09, Wait=300ms, START: 179ms
task-02 ThrID: 06, Wait=100ms, START: 178ms
task-04 ThrID: 08, Wait=100ms, START: 179ms
task-05 ThrID: 04, Wait=300ms, START: 179ms
task-06 ThrID: 07, Wait=100ms, START: 184ms
task-01 ThrID: 05, Wait=300ms, START: 178ms
task-07 ThrID: 10, Wait=300ms, START: 184ms
task-00 ThrID: 03, Wait=100ms, END: 284ms
task-08 ThrID: 03, Wait=100ms, START: 184ms
task-02 ThrID: 06, Wait=100ms, END: 285ms
task-09 ThrID: 06, Wait=300ms, START: 184ms
task-04 ThrID: 08, Wait=100ms, END: 286ms
task-06 ThrID: 07, Wait=100ms, END: 293ms
task-08 ThrID: 03, Wait=100ms, END: 385ms
task-03 ThrID: 09, Wait=300ms, END: 485ms
task-05 ThrID: 04, Wait=300ms, END: 486ms
task-01 ThrID: 05, Wait=300ms, END: 493ms
task-07 ThrID: 10, Wait=300ms, END: 494ms
task-09 ThrID: 06, Wait=300ms, END: 586ms

Edit:
With async lambda and await Task.Delay() is as fast as Thread.Sleep(), may be also faster (511ms).
Edit 2:
With ThreadPool.SetMinThreads(16, 16); Task.Delay().Wait() works as fast as Thread.Sleep for 10 iteration in the loop. With more iterations it's slower again. It's also interesting, that if without adjusting I increase the number of iterations for Thread.Sleep to 30, it's still faster, then 10 iteration with Task.Delay().Wait()
Edit 3:
The overloading Task.Delay(wait).Wait(wait) works as fast as Thread.Sleep()

Rekshino
  • 6,954
  • 2
  • 19
  • 44
  • 1
    Have you tested Task with async and await? – Ivan Ičin Dec 06 '18 at 09:34
  • @IvanIčin No, I havn't.. – Rekshino Dec 06 '18 at 09:35
  • @IvanIčin Now I have tested, see my edit.. – Rekshino Dec 06 '18 at 09:41
  • 4
    If you add `ThreadPool.SetMinThreads(16, 16);` at the start of your code, you'll see that it reduces the time to what you expect. This for the reason described in the now-deleted answer from @Damien_The_Unbeliever (i.e. once more than the min number of threads is exceeded, the threadpool delays starting each new thread by 2Kms) – Matthew Watson Dec 06 '18 at 09:51
  • @MatthewWatson Indeed! Hm.. Is `Task.Delay()` being handled with `ThreadPool`? But then why `async/await` works as fast as `Thread.Sleep` without adjusting `ThreadPool` settings? – Rekshino Dec 06 '18 at 09:57
  • More on a point above [here](https://www.infoworld.com/article/3201030/application-development/understand-the-net-clr-thread-pool.html). The key point though: _"The .Net thread pool injects or removes threads at intervals of 500 milliseconds or as a thread becomes free, whichever comes first"_ – sellotape Dec 06 '18 at 10:31
  • 2
    @Rekshino `Task.Delay().Wait()` is meaningless. [Task.Delay()](https://referencesource.microsoft.com/#mscorlib/system/threading/tasks/Task.cs,5863) will return a task that completes when a System.Threading.Timer fires. This, combined with async/await, allows delaying *without* blocking any threads. `Task.Delay().Wait()` immediately blocks the thread while waiting for the timer to fire, losing any benefit provided by `Task.Delay()` – Panagiotis Kanavos Dec 07 '18 at 20:40
  • @PanagiotisKanavos Yes, it is meaningless. But `Thread.Sleep()` also does block the thread and is 4x time faster then `Task.Delay().Wait()` – Rekshino Dec 10 '18 at 07:59
  • 2
    @Rekshino that makes the comparison multiple times meaningless. The job of `Task.Delay()` is to *not* block. It's job is to resume execution on an already existing ThreadPool thread once the timer expires, not to get blocked. – Panagiotis Kanavos Dec 10 '18 at 08:18
  • @Rekshino speaking of meaningless comparisons, you could also use SpinWait for "faster" blocking, by keeping the thread busy until the timer expires. – Panagiotis Kanavos Dec 10 '18 at 08:19
  • @Rekshino btw your code probably doesn't measure either Sleep or Wait anyway, because `Task.Run` is never awaited, it just keeps firing tasks one after the other. And yet, the *same* Stopwatch is used for *all* those tasks, without any attempt to reset it. – Panagiotis Kanavos Dec 10 '18 at 08:25
  • @PanagiotisKanavos I don't wait on tasks finished intentionally. That `Stopwatch` never being restarted is also intentionally, it gives a timeline. The question was about `Task.Delay().Wait()`, not about how to wait in tasks. Should Task.Delay() return not the task? Is it wrong design? – Rekshino Dec 10 '18 at 09:00
  • @Rekshino it's a wrong benchmark, using the wrong code to do the wrong thing. If you wanted to get real times, use BenchmarkDotNet. As for what happens, it's already been explained in comments. The TPL uses the ThreadPool. *Threads* though run on *CPUs*. When you block a thread, you block a CPU until that thread gets suspended. Blocking operations always start with a spinwait because blocking *is not supposed to take long*. `Thread.Sleep` on the other hand puts the thread to sleep immediatelly. Your code ends up blocking all cores in different ways – Panagiotis Kanavos Dec 10 '18 at 09:06
  • @Rekshino `await Task.Delay()` *avoids* that by *not* blocking at all. That's how it's supposed to be used. Tasks in general are *not* supposed to get blocked to get a result from them – Panagiotis Kanavos Dec 10 '18 at 09:10
  • @HansPassant Indeed, I ran the tests on Win10. So you mean, that it could be undocumented improvement for `Thread.Sleep()`? – Rekshino Dec 10 '18 at 10:26
  • 1
    Not exactly, undocumented improvement of the threadpool. Win10 creates its [own threadpool](https://stackoverflow.com/a/34826385/17034) immediately at startup of the program, having two pools that don't know about each other is a problem. So I'd assume they did something about it in a .NET 4.7.x update and made them work together better. Easy to do in the CLR btw, it has a hosting interface (ICorThreadPool) that allows a host to change the way the threadpool works. Complete guess, very hard to verify. – Hans Passant Dec 10 '18 at 10:41
  • I tried reproducing the benchmarks that you shared with a fiddle[ https://dotnetfiddle.net/75SIcR ], But the benchmark from the fiddle doesn't show any dramatic difference between Thread.sleep and Task.wait. will you be able to share a fiddle reproducing the benchmarks that are shared here? – krish Dec 14 '18 at 05:40
  • @krish No, I'm not able to do it with dotnetfiddle. Which OS and .NET version was used on your side? – Rekshino Dec 14 '18 at 07:28
  • @Rekshino Windows 10, dotnet core 2.1 and also tried in Fiddle, I'm unable to reproduce your benchmarks – krish Dec 14 '18 at 09:13
  • The case is reproduceable and has nothing to do with having one or two or whatever threadpools. What matters is the current capacity of the threadpool that would process the queue of work items (yes, task.Delay results in one additional work item in the thread pool). See my answer for the details. – Nick Dec 17 '18 at 09:00
  • @Nick See _Edit 2_: 30 tasks with `Tread.Sleep()` are faster then 10 => (10+10 = 20) with `Task.Delay().Wait()` – Rekshino Dec 17 '18 at 09:21
  • @Rekshino, indeed, Edits 2 and 3 confirm my theory. Edit 2 => a larger thread pool is capable of processing more work items, therefore it doesn't choke on `Task.Wait()`. Edit 3 => `Task.Delay(xxx).Wait(xxx).` is called with a time-out, which means it is not inlined and doesn't block the current worker thread completely; it spins and yields execution before blocking, and thus is able to do a bit more processing. – Nick Dec 17 '18 at 10:01
  • @Nick I wish you theory would be truth, but I see no explanation for the fact, that increasing the number of iteration with `Thread.Sleep(xxx)` to __60__ I'm still faster(~1800ms) then with __10__ iteration with `Task.Delay(xxx).Wait()`. My assumption, that it lays on managing of process's `ThreadPool` and it's interaction with system thread pool, if I'm not mistaken, the timer runs on native thread pool. – Rekshino Dec 17 '18 at 10:23
  • @Rekshino, I used the debugger to observe the behavior of the program. I tried with 'wait = 300` and 20 running tasks. When using `Thread.Delay(wait).Wait()`, the program spawns about 40 tasks. The first half is blocked on a task from the second half of the list (blocked on the `Wait` call, the internally used `ManualResetEventSlim`). The second half has tasks, which are scheduled. At the same time, the number of actual working threads is about 10. Numbers vary per the moment you manage to pause the application. Again, I believe my answer is on the right track. :-) – Nick Dec 17 '18 at 13:55

3 Answers3

8

Neither Thread.Sleep(), nor Task.Delay() guarantee that the interval will be correct.

Thread.Sleep() and Task.Delay() work very differently. Thread.Sleep() blocks the current thread and prevents it from executing any code. Task.Delay() creates a timer that will tick when the time expires and assigns it to execution on the threadpool.

You run your code by using Task.Run(), which will create tasks and enqueue them on the threadpool. When you use Task.Delay(), the current thread is released back on the thread pool, and it can start processing another task. In this way, multiple tasks will start faster and you will record startup times for all. Then, when the delay timers start ticking, they also exhaust the pool, and some tasks take quite longer to finish than since they started. That is why you record long times.

When you use Thread.Sleep(), you block the current thread on the pool and it is unable to process more tasks. The Thread pool doesn't grow immediately, so new tasks just wait. Therefore, all tasks run at about the same time, which seem faster to you.

EDIT: You use Task.Wait(). In your case, Task.Wait() tries to inline the execution on the same thread. At the same time, Task.Delay() relies on a timer that gets executed on the thread pool. Once by calling Task.Wait() you block a worker thread from the pool, second you require an available thread on the pool to complete the operation of the same worker method. When you await the Delay(), no such inlining is required, and the worker thread is immediately available to process timer events. When you Thread.Sleep, you don't have a timer to complete the worker method.

I believe this is what causes the drastic difference in the delay.

Nick
  • 4,787
  • 2
  • 18
  • 24
  • 1
    _"When you use Task.Delay(), the current thread is released back on the thread pool, and it can start processing another task"_ I doubt it strongly! I would say, that sync `Task.Delay().Wait()` blocks the thread as `Thread.Sleep()` and thread will not be reused, but in case `await Task.Delay()` the thread will be reused. – Rekshino Dec 10 '18 at 07:43
  • It doesn't seem faster to me, its __4x time__ faster. And why then `await Task.Delay()` works as fast as `Thread.Sleep()` even though also new tasks will be created? See my question - 30 iterations of `Thread.Sleep()` are faster than 10 iterations of `Task.Delay().Wait()` – Rekshino Dec 10 '18 at 07:52
  • I think the cause lays in how `Thread.Delay()` works, see how I edited my answer. Hope it helps. – Nick Dec 10 '18 at 12:02
8

I rewrote the posted snippet a bit to get the results ordered better, my brand-new laptop has too many cores to interpret the existing jumbled output well enough. Recording the start and end times of each task and displaying them after they are all done. And recording the actual start time of the Task. I got:

0: 68 - 5031
1: 69 - 5031
2: 68 - 5031
3: 69 - 5031
4: 69 - 1032
5: 68 - 5031
6: 68 - 5031
7: 69 - 5031
8: 1033 - 5031
9: 1033 - 2032
10: 2032 - 5031
11: 2032 - 3030
12: 3030 - 5031
13: 3030 - 4029
14: 4030 - 5031
15: 4030 - 5031

Ah, that suddenly makes a lot of sense. A pattern to always watch for when dealing with threadpool threads. Note how once a second something significant happens and two tp threads start running and some of them can complete.

This is a deadlock scenario, similar to this Q+A but otherwise without the more disastrous outcome of that user's code. The cause is next-to-impossible to see since it is buried in .NETFramework code, you'd have to look how Task.Delay() is implemented to make sense of it.

The relevant code is here, note how it uses a System.Threading.Timer to implement the delay. A gritty detail about that timer is that its callback is executed on the threadpool. Which is the basic mechanism by which Task.Delay() can implement the "you don't pay for what you don't use" promise.

The gritty detail is that this can take a while if the threadpool is busy churning away at threadpool execution requests. It's not the timer is slow, the problem is that the callback method just doesn't get started soon enough. The problem in this program, Task.Run() added a bunch of requests, more than can be executed at the same time. The deadlock occurs because the tp-thread that was started by Task.Run() cannot complete the Wait() call until the timer callback executes.

You can make it a hard deadlock that hangs the program forever by adding this bit of code to the start of Main():

     ThreadPool.SetMaxThreads(Environment.ProcessorCount, 1000);

But the normal max-threads is much higher. Which the threadpool manager takes advantage of to solve this kind of deadlock. Once a second it allows two more threads than the "ideal" number of them to execute when the existing ones don't complete. That's what you see back in the output. But it is only two at a time, not enough to put much of a dent in the 8 busy threads that are blocked on the Wait() call.

The Thread.Sleep() call does not have this problem, it doesn't depend on .NETFramework code or the threadpool to complete. It is the OS thread scheduler that takes care of it, and it always runs by virtue of the clock interrupt. Thus allowing new tp threads to start executing every 100 or 300 msec instead of once a second.

Hard to give concrete advice to avoid such a deadlock trap. Other than the universal advice, always avoid having worker threads block.

Hans Passant
  • 922,412
  • 146
  • 1,693
  • 2,536
  • Hmm. The answer seems to be plausible. – Rekshino Dec 17 '18 at 11:52
  • @Rekshino: I think the answer is correct - if you want to "prove" it consider subscibing to threadpool events (https://learn.microsoft.com/en-us/dotnet/framework/performance/thread-pool-etw-events). I think you will see a "ThreadPoolWorkerThreadAdjustmentAdjustment" event before the program continues. – Mads Ravn Dec 22 '18 at 17:58
2

Your problem is that you are mixing asynchronous code with synchronous code without using async and await. Don't use synchronous call .Wait, it's blocking your thread and that's why asynchronous code Task.Delay() won't work properly.

Asynchronous code often won't work properly when called synchronously because it isn't designed to work that way. You can get lucky and asynchronous code seems to work when running synchronously. But if you are using some external library author of that library can change their code in a way to will break your code. Asynchronous code should be all the way down asynchronous.

Asynchronous code is usually slower than synchronous one. But benefit is that it runs asynchronously, example if your code is waiting for file to load some other code can run on same CPU Core while that file is loading.

Your code should look like below, but with async you can't be sure that ManagedThreadId will stay the same. Because thread running your code can change during execution. You should never use ManagedThreadId property or [ThreadStatic] attribute if you using asynchronous code anyway because of that reason.

Async/Await - Best Practices in Asynchronous Programming

bool flag = true;
var sw = Stopwatch.StartNew();
for (int i = 0; i < 10; i++)
{
    var cntr = i;
    {
        var start = sw.ElapsedMilliseconds;
        var wait = flag ? 100 : 300;
        flag = !flag;

        Task.Run(async () =>
        {
            Console.WriteLine($"task-{cntr.ToString("00")} \t ThrID: {Thread.CurrentThread.ManagedThreadId.ToString("00")},\t Wait={wait}ms, \t START: {start}ms");
            await Task.Delay(wait);
            Console.WriteLine($"task-{cntr.ToString("00")} \t ThrID: {Thread.CurrentThread.ManagedThreadId.ToString("00")},\t Wait={wait}ms, \t END: {sw.ElapsedMilliseconds}ms");
        });
    }
}
Console.ReadKey();
return;
Wanton
  • 800
  • 6
  • 9
  • Thank you for your response. The question was _why Task.Delay().Wait() takes 4x more time, then Thread.Sleep()_. The explanation would be interesting. – Rekshino Dec 12 '18 at 13:22
  • 1
    Why I "_should never use ManagedThreadId property or [ThreadStatic] attribute if you using asynchronous code_"? And if I want to know on which ThreadPool-thread the task/sub task does run? – Rekshino Dec 12 '18 at 13:27
  • @Rekshino I did explain, using `Task.Delay().Wait()` is blocking your thread and your asynchronous code won't work properly. I'll edit to make it clearer. You should never use anything related to thread id when working with asynchronous code, because thread running your code can change during execution. I'll edit my answer to include that as well. – Wanton Dec 12 '18 at 16:01
  • `Thread.Sleep()` does block the thread too, but it is 4x faster. Have you read my edits to the question and comments from other people? – Rekshino Dec 12 '18 at 16:06
  • @Rekshino problem here is that you don't seem to understand how asynchronous code works. Asynchronous code often won't work properly when called synchronously because it isn't designed to work that way. I added a link to my answer, you should read that. – Wanton Dec 12 '18 at 16:12
  • I see it not as problem to run async code synchronously. E.g. see my Edit 2, if we adjust `ThreadPool` configuration, then it works the same way as with `Thread.Sleep()` And as I already have noted in my question - I'm aware about blocking the thread. – Rekshino Dec 12 '18 at 16:17
  • @Rekshino but because you are not using `await` keyword front of `Task.Delay()` it's not working properly because `Task` is not returned to `Task.Run()`. I don't know if I can help you anymore because you don't seem to understand how asynchronous code works. Maybe other people can explain it to you better than me. You should really read that link I added to my answer. – Wanton Dec 12 '18 at 16:24
  • +1 for "mixing asynchronous code with synchronous code without using async and await", no one realizes that the thread is returned back to the thread pool the instant it hits an await. – bmiller Feb 25 '22 at 01:50