Task.Delay()。Wait()发生了什么?

时间:2018-12-06 09:15:46

标签: c# .net multithreading task task-parallel-library

我感到困惑的是,为什么Task.Delay().Wait()花费的时间比以前多{strong> 4倍,然后是Thread.Sleep()

例如 task-00 仅线程9 上运行,并花费了 2193ms ? 我知道,同步等待在任务中是不好的,因为整个线程都被阻塞了。只是为了测试。

在控制台应用程序中进行简单测试:

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;

使用Task.Delay().Wait()
任务03 ThrID:05,等待= 300ms,开始:184ms
任务04阈值ID:07,等待= 100ms,开始:184ms
task-00 ThrID:09,Wait = 100ms,START:0ms
任务06 ThrID:04,等待= 100ms,开始:185ms
task-01 ThrID:08,Wait = 300ms,START:183ms
任务05阈值ID:03,等待= 300ms,开始:185ms
任务02阈值ID:06,等待= 100毫秒,开始:184毫秒
task-07 ThrID:10,Wait = 300ms,START:209ms
task-07 ThrID:10,Wait = 300ms,END:1189ms
task-08 ThrID:12,Wait = 100ms,START:226ms
任务09阈值ID:10,等待= 300ms,开始:226ms
任务09阈值ID:10,等待= 300ms,结束:2192ms
任务06 ThrID:04,等待= 100ms,END:2193ms
task-08 ThrID:12,Wait = 100ms,END:2194ms
任务05阈值ID:03,等待= 300ms,结束: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
任务04阈值ID:07,等待= 100ms,结束:2193ms
task-01 ThrID:08,Wait = 300ms,END:2193ms

使用Thread.Sleep()
task-00 ThrID:03,Wait = 100ms,START:0ms
task-03 ThrID:09,Wait = 300ms,START:179ms
任务02 ThrID:06,等待= 100ms,开始:178ms
task-04 ThrID:08,Wait = 100ms,START:179ms
任务05阈值ID:04,等待= 300ms,开始: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
任务03 ThrID:09,等待= 300ms,结束:485ms
任务05阈值ID:04,等待= 300ms,结束:486ms
task-01 ThrID:05,Wait = 300ms,END:493ms
task-07 ThrID:10,Wait = 300ms,END:494ms
任务09阈值ID:06,等待= 300ms,结束:586ms

修改:
使用async lambda和await Task.Delay()Thread.Sleep()一样快,可能也会更快(511ms)。
编辑2:
使用ThreadPool.SetMinThreads(16, 16);Task.Delay().Wait()的运行速度与Thread.Sleep一样快,可循环10次。随着迭代次数的增加,它又变慢了。有趣的是,如果不进行调整,我将Thread.Sleep的迭代次数增加到 30 ,它仍然更快,然后使用Task.Delay().Wait()进行 10 迭代
编辑3:
重载Task.Delay(wait).Wait(wait)的运行速度与Thread.Sleep()

一样快

3 个答案:

答案 0 :(得分:7)

我稍微重写了一段代码,以使结果更好地排序,我的全新笔记本电脑的内核太多,不足以充分解释现有的混乱输出。记录每个任务的开始和结束时间,并在完成所有任务后显示它们。并记录任务的实际开始时间。我得到了:

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

嗯,这突然变得很有意义。处理线程池线程时始终要注意的一种模式。请注意,每秒钟发生一次重大事件,并且两个tp线程开始运行,并且其中一些线程可以完成。

这是一个僵局,类似于this Q+A,但在其他情况下该用户的代码没有更灾难性的结果。由于它是埋在.NETFramework代码中,因此原因几乎是不可能的,您必须查看如何实现Task.Delay()才能理解它。

相关代码is here,请注意它如何使用System.Threading.Timer来实现延迟。关于该计时器的一个棘手的细节是它的回调在线程池上执行。这是Task.Delay()可以实现“您不为不使用的商品付费”承诺的基本机制。

棘手的细节是,如果线程池忙于处理线程池执行请求,这可能需要一段时间。这不是计时器很慢,问题在于回调方法还没有足够快地启动。该程序中的问题Task.Run()添加了一堆请求,超过了可以同时执行的请求数量。之所以会出现死锁,是因为由Task.Run()启动的tp线程在计时器回调执行之前无法完成Wait()调用。

通过将以下代码添加到Main()的开头,可以使它永久死机,使其永久挂起程序:

     ThreadPool.SetMaxThreads(Environment.ProcessorCount, 1000);

但是正常的最大线程数要高得多。线程池管理器利用它来解决这种死锁。当现有线程未完成时,它每秒执行一次的线程数将比“理想”数量多两个。这就是您在输出中看到的内容。但这一次只有两个,不足以在Wait()调用中阻塞的8个繁忙线程中占用很多凹痕。

Thread.Sleep()调用不存在此问题,它不依赖于.NETFramework代码或线程池来完成。它是由OS线程调度程序负责的,并且它总是通过时钟中断来运行。因此,允许新的tp线程开始每100或300毫秒执行一次,而不是每秒执行一次。

很难给出具体建议以避免这种死锁陷阱。除了通用建议以外,请始终避免阻塞工作线程。

答案 1 :(得分:5)

Thread.Sleep()Task.Delay()都不保证内部内容正确。

Thread.Sleep()的工作Task.Delay()非常不同。 Thread.Sleep()阻止当前线程并阻止其执行任何代码。 Task.Delay()创建一个计时器,该计时器将在时间到期时计时,并将其分配给线程池执行。

您可以使用Task.Run()运行代码,这将创建任务并将其排队在线程池中。当您使用Task.Delay()时,当前线程被释放回线程池,并且它可以开始处理另一个任务。这样,多个任务将更快地启动,并且您将记录所有任务的启动时间。然后,当延迟计时器开始计时时,它们也耗尽了池,并且某些任务完成的时间比开始时要长。这就是为什么要长时间录制的原因。

使用Thread.Sleep()时,您将阻塞池中的当前线程,并且该线程无法处理更多任务。线程池不会立即增长,因此只需等待新任务。因此,所有任务大约同时运行,对您来说似乎更快。

编辑:您使用Task.Wait()。 在您的情况下,Task.Wait()尝试内联同一线程上的执行。同时,Task.Delay()依赖在线程池上执行的计时器。一旦调用Task.Wait(),您就从池中阻止了一个工作线程,然后,您需要池中有一个可用线程来完成同一worker方法的操作。当await Delay()时,不需要这种内联,并且工作线程可立即用于处理计时器事件。 Thread.Sleep时,您没有计时器来完成worker方法。

我相信这是造成延迟差异巨大的原因。

答案 2 :(得分:0)

您的问题是您在不使用asyncawait的情况下将异步代码与同步代码混合在一起。不要使用同步调用.Wait,它会阻塞您的线程,这就是异步代码Task.Delay()无法正常工作的原因。

异步代码通常在同步调用时无法正常工作,因为它不是设计为那样工作的。您可以得到幸运的消息,并且异步运行时异步代码似乎可以正常工作。但是,如果您正在使用某个外部库,则该库的作者可以更改其代码,从而破坏您的代码。异步代码应该一直是异步的。

异步代码通常比同步代码慢。但是好处是它可以异步运行,例如,如果您的代码正在等待文件加载,则其他一些代码可以在该文件加载时在同一CPU Core上运行。

您的代码应如下所示,但是使用async不能确保ManagedThreadId保持不变。因为运行代码的线程可以在执行期间更改。因此,如果您始终使用异步代码,则永远不要使用ManagedThreadId属性或[ThreadStatic]属性。

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;