是否有很好的规则来说明何时使用Task。Delay vs . Thread.Sleep?

具体来说,是否存在一个最小值来保证其中一个比另一个更有效? 最后,自从任务。延迟导致异步/等待状态机上的上下文切换,是否有使用它的开销?


当前回答

我想补充一点。 实际上,任务。Delay是一种基于定时器的等待机制。如果您查看源代码,您会发现一个Timer类的引用,它是造成延迟的原因。另一方面,线程。Sleep实际上使当前线程休眠,这样你只是阻塞和浪费了一个线程。在异步编程模型中,如果你想在延迟后发生某些事情(延续),你应该总是使用Task.Delay()。

其他回答

我想补充一点。 实际上,任务。Delay是一种基于定时器的等待机制。如果您查看源代码,您会发现一个Timer类的引用,它是造成延迟的原因。另一方面,线程。Sleep实际上使当前线程休眠,这样你只是阻塞和浪费了一个线程。在异步编程模型中,如果你想在延迟后发生某些事情(延续),你应该总是使用Task.Delay()。

使用线程。当您想要阻塞当前线程时休眠。

使用await Task。延迟:当您希望在不阻塞当前线程的情况下进行逻辑延迟时。

效率不应该是这些方法的首要考虑因素。它们在现实世界中的主要用途是作为I/O操作的重试计时器,其数量级为秒而不是毫秒。

任务之间最大的区别。延迟和线程。睡眠就是那个任务。Delay旨在异步运行。使用Task没有意义。同步代码中的延迟。使用线程是一个非常糟糕的主意。在异步代码中休眠。

通常你会用await关键字调用Task.Delay():

await Task.Delay(5000);

或者,如果你想在延迟之前运行一些代码:

var sw = new Stopwatch();
sw.Start();
Task delay = Task.Delay(5000);
Console.WriteLine("async: Running for {0} seconds", sw.Elapsed.TotalSeconds);
await delay;

猜猜这会打印什么?运行0.0070048秒。 如果我们将await延迟移到控制台上方。而是写入eline,它将打印运行5.0020168秒。

让我们看看Thread的不同之处。睡眠:

class Program
{
    static void Main(string[] args)
    {
        Task delay = asyncTask();
        syncCode();
        delay.Wait();
        Console.ReadLine();
    }

    static async Task asyncTask()
    {
        var sw = new Stopwatch();
        sw.Start();
        Console.WriteLine("async: Starting");
        Task delay = Task.Delay(5000);
        Console.WriteLine("async: Running for {0} seconds", sw.Elapsed.TotalSeconds);
        await delay;
        Console.WriteLine("async: Running for {0} seconds", sw.Elapsed.TotalSeconds);
        Console.WriteLine("async: Done");
    }

    static void syncCode()
    {
        var sw = new Stopwatch();
        sw.Start();
        Console.WriteLine("sync: Starting");
        Thread.Sleep(5000);
        Console.WriteLine("sync: Running for {0} seconds", sw.Elapsed.TotalSeconds);
        Console.WriteLine("sync: Done");
    }
}

试着预测一下它会打印什么…

异步:开始 async:运行0.0070048秒 同步:开始 async:运行5.0119008秒 异步:完成 sync:运行5.0020168秒 同步:完成

另外,有趣的是注意到Thread。睡眠要精确得多,毫秒精度不是真正的问题,而任务。延迟最少需要15-30毫秒。与ms精度相比,这两个函数的开销是最小的(如果你需要更精确的东西,请使用Stopwatch Class)。线程。睡眠仍然束缚着你的线程,任务。在等待的同时,延迟释放它以做其他工作。

我和一位同事为此争论了很长时间,他向我证明,在上面的答案目前所显示的范围之外,还有显著的差异。如果你等待Task.Delay(somemillisecseconds),你实际上可以释放堆栈上的直接父对象之外的调用者:

using System;
using System.Threading;
using System.Threading.Tasks;

namespace ConsoleApp1
{
    class Program
    {
        static async Task Main(string[] args)
        {
            Console.WriteLine("Started " + Thread.CurrentThread.ManagedThreadId);
            DoSomething1();
            Console.WriteLine("Finished " + Thread.CurrentThread.ManagedThreadId);
            Thread.Sleep(6000);
        }

        static async void DoSomething1()
        {
            Console.WriteLine("DoSomething1 Started " + Thread.CurrentThread.ManagedThreadId);
            var result = await DoSomething2();
            Console.WriteLine("DoSomething1 Finished " + Thread.CurrentThread.ManagedThreadId);
        }

        static async Task<int> DoSomething2()
        {
            Console.WriteLine("DoSomething2 Started " + Thread.CurrentThread.ManagedThreadId);

            await Task.Delay(5000);         // Will block DoSomething1 but release Main
            //Thread.Sleep(5000);           // Will block everything including Main
            //await Task.FromResult(5);     // Will return immediately (just for comparison)
            //await Task.Delay(0);          // What will it do, can you guess?

            Console.WriteLine("DoSomething2 Finished " + Thread.CurrentThread.ManagedThreadId);
            return 0;
        }
    }
}

玩一下这段代码,观察使用Delay或Sleep的不同效果。这个解释超出了这个答案的范围,但可以总结为“异步函数不会启动另一个线程,直到它们等待一些不能立即运行的东西(或结果确定)”。输出如下:

Started 1
DoSomething1 Started 1
DoSomething2 Started 1
Finished 1
DoSomething2 Finished 4
DoSomething1 Finished 4

这不是关于DoSomething1();在Main被大火遗忘。你可以用Sleep来证明这一点。还要观察当DoSomething2从Task“返回”时。延迟,它在另一个线程上运行。

这个东西比我给它的信用要聪明得多,相信await只是开始了一个新的线程来做事情。我仍然没有假装完全理解,但上面的反直觉结果表明,在底层有更多的事情要做,而不仅仅是启动线程来运行代码。

在异步程序中,两者的区别

await task.Delay() 
//and 
thread.sleep 

在一个简单的应用程序中,一个可能更可取消,一个可能更准确,一个可能更快……但在一天结束的时候,两者都做同样的事情,他们阻塞正在执行的代码…

以下是调查结果:

1 00:00:00.0000767
Not Delayed.
1 00:00:00.2988809
Delayed 1 second.
4 00:00:01.3392148
Delayed 3 second.
5 00:00:03.3716776
Delayed 9 seconds.
5 00:00:09.3838139
Delayed 10 seconds
4 00:00:10.3411050
4 00:00:10.5313519

从这段代码:

var sw = new Stopwatch();
sw.Start();
Console.WriteLine($"{sw.Elapsed}");
var asyncTests = new AsyncTests();

var go1 = asyncTests.WriteWithSleep();
var go2 = asyncTests.WriteWithoutSleep();

await go1;
await go2;
sw.Stop();
Console.WriteLine($"{sw.Elapsed}");
        
Stopwatch sw1 = new Stopwatch();
Stopwatch sw = new Stopwatch();
    public async Task WriteWithSleep()
    {
        sw.Start();
        var delayedTask =  Task.Delay(1000);
        Console.WriteLine("Not Delayed.");
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} {sw.Elapsed}");
        await delayedTask;
        Console.WriteLine("Delayed 1 second.");
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} {sw.Elapsed}");
        Thread.Sleep(9000);
        Console.WriteLine("Delayed 10 seconds");
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} {sw.Elapsed}");
        sw.Stop();
    }
    public async Task WriteWithoutSleep()
    {
        await Task.Delay(3000);
        Console.WriteLine("Delayed 3 second.");
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} {sw.Elapsed}");
        await Task.Delay(6000);
        Console.WriteLine("Delayed 9 seconds.");
        Console.WriteLine($"{Thread.CurrentThread.ManagedThreadId} {sw.Elapsed}");
    }

Sleep acts the same way as an immediate await, except it blocks the thread. A task that is assigned to a var may cause a thread switch when it is finally awaited. In this example, it looks like the code starts on thread 1, then creates thread 5 for WriteWithoutSleep(), but continues executing on thread 1 for ThreadWithSleep() until delayedTask is awaited. At that moment, thread 1's code flows into thread 4 and further execution in Main is now on thread 4; thread 1 is for lack of a better word thrown away.

以上所有的答案都很有价值。然而,在一个简单的控制台应用程序中,它似乎并不重要,除非在你使用的几次运行过程中,如果你立即等待你的Task.Delay()并且不打算使用取消令牌;

在一个复杂的应用程序中,让线程进入睡眠状态,还是因为创建任务而从一个线程跳到另一个线程,然后等待它们,还是立即等待,这是需要考虑的问题。

最后,放入Process.GetCurrentProcess(). threads。控制台应用程序(至少是我的)开头的Count在调试器模式下产生了13个线程。在等待调用之后,我在visual studio的调试器模式下有17个线程。我曾经读到consoleApp只有3个线程,其余的都是调试器线程,但是在没有调试的情况下运行consoleApp会导致8个线程和14个线程。在visual studio外部运行它的结果是8个线程和14个线程。

复制代码并粘贴它之后,线程数是相同的,8,14,所有内容都停留在线程4和5上。第二个线程。Sleep和task.delay不会导致线程跳转。所有这些研究都是为了提出:while线程。Sleep会阻塞一个线程,task.delay不会,并且有一个取消令牌,除非你的应用程序非常复杂,它真的不重要,因为表面上:task.delay和thread。睡眠也做着同样的事情。