什么时候使用Task.Delay,什么时候使用Thread.Sleep?

617

何时使用 Task.Delay 而不是 Thread.Sleep 有哪些好的规则?

  • 具体来说,是否存在最小值,可以提高一个方法的有效性和效率而不是另一个方法?
  • 最后,由于 Task.Delay 在异步/等待状态机上引起上下文切换,因此使用它是否会产生开销?

4
在计算机世界中,10毫秒是很多周期。 - Brad Christie
它应该有多快?你有哪些性能问题? - L.B
4
我认为更为相关的问题是你打算在什么情境下使用它们?没有这些信息,范围太广了。你所说的有效/高效是指什么?你是指准确度、功率效率等吗?我非常好奇这在什么情境下很重要。 - James World
5
最小值为15.625毫秒,低于时钟中断速率的值无效。Task.Delay总是会消耗一个System.Threading.Timer,Sleep没有开销。当编写不执行任何操作的代码时,您不必担心开销。 - Hans Passant
4
有一件我觉得很重要但没有看到提到的事情是,Task.Delay 支持 CancellationToken,这意味着如果你在使用它来减缓循环处理过程时,可以通过取消令牌来中断延迟。这也意味着当你想要取消时,你的进程可以快速响应。但你也可以通过缩短线程休眠周期间隔并手动检查令牌来实现相同的效果。 - Droa
https://social.technet.microsoft.com/wiki/contents/articles/21177.visual-c-thread-sleep-vs-task-delay.aspx - Mike Lowery
10个回答

606

当您想要阻塞当前线程时,请使用Thread.Sleep

当您需要逻辑延迟而不阻塞当前线程时,请使用await Task.Delay

这些方法的效率不应是首要问题。它们在现实世界中的主要用途是作为I/O操作的重试计时器,这些操作的时间范围在秒而非毫秒级别。


4
同一个主要用例:重试定时器。 - Stephen Cleary
4
或者当您不想在主循环中消耗CPU时。 - Eddie Parker
7
没有“其他线程”,它内部是用计时器实现的。 - Stephen Cleary
84
建议不要担心效率是不明智的。Thread.Sleep 将阻塞当前线程,导致上下文切换。如果您正在使用线程池,则这也可能导致分配新线程。这两个操作都非常耗费资源,而 Task.Delay 等协作式多任务提供了避免所有这些开销、最大化吞吐量、允许取消并提供更清洁的代码的设计。 - Corillian
7
@LucaCremry onesi: 我会在同步方法中使用Thread.Sleep来等待。然而,在生产代码中我从未这样做过;根据我的经验,我所见过的每个Thread.Sleep`都表明存在某种需要得到适当修复的设计问题。 - Stephen Cleary
显示剩余13条评论

376

Task.DelayThread.Sleep 最大的区别是,Task.Delay 旨在异步运行。在同步代码中使用 Task.Delay 是没有意义的。在异步代码中使用 Thread.Sleep 是非常糟糕的想法。

通常你会用 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 delay移到Console.WriteLine之前,它会输出“Running for 5.0020168秒”。

现在来看看使用Thread.Sleep的区别:

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: 开始
async: 运行了0.0070048秒
sync: 开始
async: 运行了5.0119008秒
async: 完成
sync: 运行了5.0020168秒
sync: 完成

同时,值得注意的是Thread.Sleep比较精确,毫秒级别的精度并不是问题,而Task.Delay最少需要15-30毫秒。这两个函数的开销与它们拥有的毫秒级别的准确性相比是很小的(如果需要更高的准确性,请使用 Stopwatch类)。Thread.Sleep仍然会占用线程,而Task.Delay则会释放线程以便进行其他工作。


43
在异步代码中使用Thread.Sleep是一个非常糟糕的想法,因为它会阻塞线程并且可能导致性能下降。 - sunside
125
异步代码的一个主要优点是允许一个线程同时处理多个任务,避免阻塞调用。这避免了大量单独线程的需要,并且允许线程池同时服务于多个请求。然而,由于异步代码通常在线程池上运行,不必要地阻塞单个线程使用 Thread.Sleep() 会消耗整个可以在其他地方使用的线程。如果使用 Thread.Sleep() 运行许多任务,则有很高的可能性耗尽所有线程池线程并严重影响性能。 - Ryan
1
明白了。我之前没有意识到异步代码的概念,就是指鼓励使用async方法。在线程池线程中运行Thread.Sleep()基本上是个坏主意,但总体来说并不是一个坏主意。毕竟,在采用(虽然不被鼓励的)Task.Factory.StartNew()路线时,有TaskCreationOptions.LongRunning选项。 - sunside
4
Tasl.Delay 的文档说明是使用系统计时器。由于“系统时钟”以恒定速率“滴答”运行,所以系统计时器的滴答速度约为16毫秒,你请求的任何延迟都将舍入为系统时钟的滴答数,加上到第一个滴答的时间偏移量。请参阅 Task.Delay 的 msdn 文档,向下滚动至备注。 - Dorus
1
@I.Step 确实,在这里没有锁定,两个进程都是独立调度的,因此最后两组代码行可以颠倒顺序。如果你看时钟,你会发现它们非常接近地同时发生。正如我之前所写的,精度只有15-30毫秒左右,所以任何比这更接近的调度都不能保证顺序。我们在这里不使用实时操作系统。 - Dorus
显示剩余17条评论

41

我想要补充一些内容。 实际上,Task.Delay 是一种基于计时器的等待机制。如果你查看源代码,你会发现引用了一个 Timer 类负责延迟。另一方面,Thread.Sleep 会让当前线程睡眠,这样你只是在阻塞并浪费一个线程。在异步编程模型中,如果你想要延迟后执行某些操作(继续),应该始终使用 Task.Delay()


1
“await Task.Delay()”允许线程在计时器到期之前执行其他任务,这一点非常明确。但是如果方法没有添加“async”前缀,我无法使用“await”,那该怎么办呢?那么我只能调用“Task.Delay()”。这种情况下,线程仍然会被阻塞,但我可以通过取消Delay()来获得优势。这样说对吗? - Erik Stroeken
9
您可以将取消标记传递给线程和任务。如果不使用await,Task.Delay()会只创建任务,而Task.Delay().Wait()会阻塞当前线程。您可以自行决定如何处理该任务,但线程会继续执行。 - user2026256

33

2
假设我们有以下情况:await Task.Delay(5000)。当我终止任务时,我会得到TaskCanceledException异常(并将其抑制),但是我的线程仍然存在。真棒! :) - AlexMelw
一个正在睡眠的线程可以通过Thread.Interrupt()方法被唤醒。这将导致sleep方法抛出InterruptException异常。https://learn.microsoft.com/en-us/dotnet/api/system.threading.thread.interrupt?view=netcore-3.1 - Josh

10
Delayed可能更适合作为Task.Delay的名称 - 因为它不会延迟现有任务,而是创建一个新的“延迟”任务,可以等待并且会使当前任务体挂起。它本质上就像一个没有回调/任务体的定时器。
等待一个延迟任务将在异步消息队列中创建一个新项,并且不会阻塞任何线程。如果存在其他任务,调用await的线程将继续处理这些任务,并在超时(或队列中前面的项完成)后返回到await点。在幕后,任务使用线程 - 单个线程中可以安排和执行许多任务。另一方面,如果你调用了Thread.Sleep(),该线程将被阻塞,即在所请求的时间内将退出并不会处理来自队列的任何异步消息。
在.NET中,有两种主要的并行处理方法,旧的基于线程、线程池等,新的则基于任务、async/await 和TPL。通常情况下,不应该混合使用这两个不同的 API 。

3
是的,在何时使用Task.Delay和Thread.Sleep方面有一些通用指南。在C#异步编程中,Task.Delay是首选方法,因为它允许您在等待时进行异步操作而不会阻塞线程。而Thread.Sleep会阻塞调用线程,并且在具有高并发性的应用程序中可能会导致性能问题。
对于Task.Delay有效或高效的最小值并不存在。总的来说,如果需要等待一定时间,最好使用Task.Delay。如果需要阻塞线程一段特定时间,请使用Thread.Sleep。
至于在async/await状态机中使用Task.Delay所带来的开销,存在一些上下文切换的开销。然而,这种开销通常很小,并且被异步编程的优点所抵消。当正确使用Task.Delay时,它可以帮助提高应用程序的响应性和可伸缩性。

0

值得一提的是,Thread.Sleep(1) 会更快地触发 GC。

这纯粹基于我和团队成员的观察。假设您有一个服务,每个特定请求都会创建新任务(大约200-300个正在进行),并且此任务在流程中包含许多弱引用。该任务像状态机一样工作,因此我们在状态更改时触发 Thread.Sleep(1),通过这样做,我们成功地优化了应用程序中的内存利用率 - 正如我之前所说 - 这将使 GC 更快地触发。在低内存消耗服务(<1GB)中,这没有太大区别。


2
嗨,Jacek。这是实验观察吗?如果不是,你能提供这个知识的来源吗? - Theodor Zoulias
1
这纯粹是我和团队成员的观察。假设您有一个服务,为特定请求创建新任务(大约200-300个正在进行),并且此任务在流程中包含许多弱引用。该任务像状态机一样工作,因此我们在更改状态时触发Thread.Sleep(1),通过这样做,我们成功地优化了应用程序中的内存利用率 - 正如我之前所说 - 这将使GC更快地启动。在低内存消耗服务(<1GB)中,这没有太大区别。 - Jacek

-1

我和同事就这个问题进行了长时间的争论,他向我证明了除目前最佳答案所示外还有显著的区别。如果你 await Task.Delay(SomeMilliseconds),实际上可以释放调用栈上除直接父级之外的其他调用者:

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

这不是关于在Main中使用DoSomething1();时的“fire and forget”。您可以通过使用Sleep来证明这一点。同时请注意,当DoSomething2从Task.Delay“返回”时,它正在另一个线程上运行。

这些东西比我想象的要聪明得多,我曾认为await只是启动了一个新线程来执行任务。我仍然不敢自称完全理解它,但上面反直觉的结果表明,在代码下面有更多的事情发生,而不仅仅是启动线程来运行代码。


async void DoSomething1() <== 避免使用 async void。它适用于事件处理程序,而 DoSomething1 看起来并不像一个事件处理程序。 - Theodor Zoulias
@TheodorZoulias 是的,这是演示代码,旨在展示如果基础调用程序(或堆栈上方的某个人)是事件处理程序可能会发生什么。如果您想查看行为差异,也可以更改它。 - Derf Skren
1
如果由于某些原因您不得不使用不良实践,您应该在答案中解释原因,并警告读者特定不良实践的危险性。否则,您正在教授人们以后必须放弃的习惯。 - Theodor Zoulias

-2
在异步程序中,区别在于
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}");
    }

睡眠的作用与立即等待一样,只是它会阻塞线程。被分配给变量的任务在最终等待时可能会导致线程切换。在此示例中,代码似乎从线程1开始,然后创建线程5以进行WriteWithoutSleep(),但在ThreadWithSleep()上继续在线程1上执行,直到延迟任务被等待。在那一刻,线程1的代码流入线程4,并且主线程中进一步的执行现在在线程4上;线程1被放弃了。

以上所有答案都非常有价值。然而,在简单的控制台应用程序中,似乎并不重要,除非您立即等待Task.Delay()并且不打算使用取消令牌;

在复杂的应用程序中,将线程置于休眠状态与由于创建任务并稍后等待它们而跳转线程与立即等待可能是需要考虑的点。

最后,在控制台应用程序(至少是我的)开头放置Process.GetCurrentProcess().Threads.Count在调试器模式下产生了13个线程。在等待调用之后,我在Visual Studio的调试器模式下有17个线程。我读过ConsoleApps只有3个线程,其余都是调试器线程,但在Visual Studio中不调试运行ConsoleApp会导致8个线程,然后是14个线程。在Visual Studio外部运行它会导致8个线程,然后是14个线程。

复制代码并将其粘贴到其后面具有相同数量的线程,即8,14,并且所有内容都保留在第4和第5个线程上。第二个thread.sleep和task.delays不会导致线程跳转。所有这些研究的目的是提出:虽然thread.sleep会阻塞线程,而task.delay不会并且具有取消令牌,但除非您的应用程序非常复杂,否则它实际上并不重要,因为表面上看,task.delay和thread.sleep几乎做相同的事情。


2
据我所知,await task.Delay()和阻塞执行线程的作用是一样的,但它并不会阻塞任何线程。如果您愿意,我可以轻松证明这一点。 - Theodor Zoulias
3
Patrick 在这里 证明了 await task.Delay() 不会 阻塞线程。我创建了 100,000 个任务,它们都在等待 await task.Delay(1000),然后等待它们全部完成。它们在 1000 毫秒后全部完成。如果每个 await task.Delay() 会阻塞一个线程,那我就需要 100,000 个线程。每个线程需要 1MB 的 RAM,所以我需要 100 GB 的 RAM。可是我的机器只有 4GB 的 RAM。所以我的程序不可能创建那么多线程。 - Theodor Zoulias
2
Patrick的回答包含这段文字:“它们都做同样的事情,它们阻塞执行线程...”。有多少人会读到这句话并理解你不是在字面上谈论被阻塞的线程,而是在谈论暂停的执行流?并不是很多人。正确理解术语非常重要,否则人们可能会从你的回答中学到一些以后需要重新学习的东西。 - Theodor Zoulias
1
@TheodorZoulias谢谢您的纠正:我已经更新为:“两者都可以做相同的事情,即阻止执行代码...” - Patrick Knott
1
“当一个任务被分配给一个变量时,当最终等待时,会导致线程切换。”这并不总是正确的。例如,在WinForms应用程序中,如果在“Click”事件的异步处理程序中使用await Task.Delay(1000),则同一线程将在“await”之后运行代码。这是因为在所有WinForms应用程序的UI线程上安装了特殊的“同步上下文”。 - Theodor Zoulias
显示剩余4条评论

-2

我的观点是,Task.Delay() 是异步的。它不会阻塞当前线程。您仍然可以在当前线程中执行其他操作。它返回一个 Task 类型(Thread.Sleep() 不返回任何内容)。您可以在另一个耗时较长的过程后稍后检查此任务是否已完成(使用 Task.IsCompleted 属性)。

Thread.Sleep() 没有返回类型。它是同步的。在线程中,除了等待延迟完成,您实际上不能做任何事情。

至于实际应用,我已经编程 15 年了。我从未在生产代码中使用过 Thread.Sleep()。我找不到任何用例。可能是因为我主要开发 Web 应用程序。


1
请注意:如果您编写“await Task.Delay()”,它将再次变为同步。我认为这个说法不正确。当然,它是异步的,因为线程可以继续运行调用者代码,并且在延迟完成后,某个时候将有一个线程接管此工作。 - Josh

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接