What is going on with Task.Delay().Wait()?

后端 未结 3 2482
天命终不由人
天命终不由人 2021-02-20 09:59

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

E.g. task-00 was running on

相关标签:
3条回答
  • 2021-02-20 10:06

    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.

    0 讨论(0)
  • 2021-02-20 10:10

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

    Thread.Sleep() work Task.Delay() 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.

    0 讨论(0)
  • 2021-02-20 10:21

    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;
    
    0 讨论(0)
提交回复
热议问题