I\'m confused why Task.Delay().Wait()
takes 4x more time, then Thread.Sleep()
?
E.g. task-00 was running on
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.