ChatGPT解决这个技术问题 Extra ChatGPT

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

何时使用 Task.DelayThread.Sleep 是否有良好的规则?

具体来说,是否有一个最小值来规定一个比另一个有效/高效?

最后,由于 Task.Delay 导致异步/等待状态机上的上下文切换,使用它是否有开销?

10ms在计算机世界中是很多周期...
它应该有多快?你有什么性能问题?
我认为更相关的问题是您打算在什么情况下使用其中任何一种?没有这些信息,范围太广了。你所说的有效/高效是什么意思?您指的是准确性,功率效率等吗?我很想知道这在什么情况下很重要。
最小值为 15.625 毫秒,小于时钟中断率的值无效。 Task.Delay 总是烧掉一个 System.Threading.Timer,Sleep 没有开销。当您编写什么都不做的代码时,您不必担心开销。
我没有看到提到的东西,但我认为很重要的是 Task.Delay 支持 CancellationToken,这意味着您可以中断延迟,例如,如果您正在使用它来减慢循环过程。这也意味着当您想取消它时,您的流程可以快速响应。但是您可以通过 Thread.Sleep 缩短睡眠周期间隔来实现相同的目的,并检查 Token manuallay。

B
Bakudan

当您想要阻止当前线程时使用 Thread.Sleep

如果您想要一个逻辑延迟而不阻塞当前线程,请使用 Task.Delay

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


这是相同的主要用例:重试计时器。
或者当您不想在主循环中消耗 CPU 时。
@RoyiNamir:不。没有“其他线程”。在内部,它是通过计时器实现的。
不担心效率的建议是不明智的。 Thread.Sleep 将阻塞导致上下文切换的当前线程。如果您使用的是线程池,这也可能导致分配一个新线程。这两个操作都非常繁重,而 Task.Delay 等提供的协作多任务处理旨在避免所有这些开销、最大化吞吐量、允许取消并提供更简洁的代码。
@LucaCremry onesi: I would use Thread.Sleep` 在同步方法中等待。但是,我从不在生产代码中这样做;根据我的经验,我见过的每一个Thread.Sleep都表明存在某种需要适当修复的设计问题。
T
Tarik

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 seconds。

让我们看看与 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");
    }
}

尝试预测这将打印什么...

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

此外,有趣的是,Thread.Sleep 更准确,毫秒精度并不是真正的问题,而 Task.Delay 最少需要 15-30 毫秒。与它们的毫秒精度相比,这两个函数的开销都是最小的(如果您需要更准确的东西,请使用 Stopwatch 类)。 Thread.Sleep 仍会占用您的线程,Task.Delay 在您等待时释放它以执行其他工作。


为什么“在异步代码中使用 Thread.Sleep 是一个非常糟糕的主意”?
@sunside 异步代码的主要优点之一是通过避免阻塞调用,允许一个线程同时处理多个任务。这避免了对大量单独线程的需求,并允许线程池同时为许多请求提供服务。但是,鉴于异步代码通常在线程池上运行,使用 Thread.Sleep() 不必要地阻塞单个线程会消耗整个线程,否则该线程可以在其他地方使用。如果使用 Thread.Sleep() 运行许多任务,则很有可能耗尽所有线程池线程并严重影响性能。
得到它。在鼓励使用 async 方法时,我错过了异步代码的概念。在线程池线程中运行 Thread.Sleep() 基本上只是一个坏主意,一般来说不是一个坏主意。毕竟,走(尽管不鼓励)Task.Factory.StartNew() 路线时会有 TaskCreationOptions.LongRunning
您可以在异步代码中将 Thread.Sleep 用于非常低的值,例如 Thread.Sleep(50)(用于某些硬件通信)。
@Reyhn 关于此的文档是 Tasl.Delay 使用系统计时器。由于“系统时钟以恒定速率“滴答”,系统计时器的滴答速度约为 16 毫秒,您请求的任何延迟都将四舍五入为系统时钟的滴答数,偏移到第一个时钟的时间打钩。请参阅 Task.Delay docs.microsoft.com/en-us/dotnet/api/… 上的 msdn 文档并向下滚动到备注。
c
crypted

我想补充一点。实际上,Task.Delay 是一个基于计时器的等待机制。如果您查看 source,您会发现对导致延迟的 Timer 类的引用。另一方面 Thread.Sleep 实际上使当前线程进入睡眠状态,这样你只是阻塞和浪费了一个线程。在异步编程模型中,如果您希望在延迟后发生某些事情(继续),您应该始终使用 Task.Delay()


'await Task.Delay()' 释放线程做其他事情,直到计时器到期,100% 清除。但是如果我不能使用'await',因为该方法没有以'async'为前缀?然后我只能调用'Task.Delay()'。在那种情况下,线程仍然被阻塞,但我有取消延迟()的优势。那是对的吗?
@ErikStroeken您可以将取消令牌传递给线程和任务。 Task.Delay().Wait() 将阻塞,而 Task.Delay() 只是在没有等待的情况下创建任务。您对该任务的处理取决于您,但线程仍在继续。
M
Mr Balanikas

如果当前线程被杀死并且您使用 Thread.Sleep 并且它正在执行,那么您可能会得到一个 ThreadAbortException。使用 Task.Delay,您始终可以提供取消令牌并优雅地终止它。这就是我选择 Task.Delay 的原因之一。见http://social.technet.microsoft.com/wiki/contents/articles/21177.visual-c-thread-sleep-vs-task-delay.aspx

我也同意在这种情况下效率并不是最重要的。


假设我们遇到以下情况:await Task.Delay(5000)。当我终止任务时,我得到 TaskCanceledException (并抑制它)但我的线程仍然存在。整洁的! :)
可以使用 Thread.Interrupt() 唤醒处于睡眠状态的线程。这将导致 sleep 抛出 InterruptException。 docs.microsoft.com/en-us/dotnet/api/…
p
pwrgreg007

Delayed 将是 Task.Delay 的更好名称 - 因为它不会延迟现有任务,而是创建一个新的“延迟”任务,另一方面,它可以等待并可能导致当前任务的主体暂停。它本质上是一个计时器,但没有回调/正文。

等待延迟任务会在异步消息队列中创建一个新项目,并且不会阻塞任何线程。如果有任何其他任务,调用 await 的同一线程将继续处理其他任务,并在超时后(或队列中的前面项目完成时)返回到等待点。后台任务使用线程 - 可以在单个线程中安排和执行许多任务。另一方面,如果您碰巧调用了 Thread.Sleep(),线程将阻塞,即它会在请求的时间内停止运行,并且不会处理来自队列的任何异步消息。

在 .NET 中有两种主要的并行方法。旧的有线程、线程池等。新的基于任务、异步/等待、TPL。根据经验,您不会混合来自这两个领域的 API。


J
Jacek

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

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


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

我与一位同事就此进行了长时间的争论,他向我证明,除了目前的最佳答案之外,存在显着差异。如果您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(); 无关。您可以通过使用 Sleep 来证明这一点。另请注意,当 DoSomething2 从 Task.Delay “返回”时,它在不同的线程上运行。

这个东西比我认为的要聪明得多,因为我相信 await 刚刚开始了一个新线程来做一些事情。我仍然没有假装理解这一切,但上面的反直觉结果表明,除了启动线程来运行代码之外,还有更多的事情要做。


async void DoSomething1() <== Avoid async void。它适用于事件处理程序,DoSomething1 看起来不像事件处理程序。
@TheodorZoulias 是的,这是演示代码,旨在显示如果基本调用者(或堆栈中的某人)是事件处理程序会发生什么。如果您想查看行为的差异,也可以更改它。
Derf Skren 如果由于某种原因必须使用不良做法,则应在答案中说明原因,并警告读者特定不良做法的危险。否则,您正在向人们学习后来必须忘记的习惯。
I
Ike

我的意见,

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

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

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


注意:如果你写“await Task.Delay()”,它会再次同步。 - 我不认为这种说法是正确的。当然它是异步的,因为线程可以自由地继续运行调用者代码,并且在将来的某个时候,一个线程将在延迟完成后接手这项工作
P
Patrick Knott

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

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}");
    }

睡眠的作用与立即等待相同,只是它阻塞了线程。分配给 var 的任务在最终等待时可能会导致线程切换。在此示例中,代码从线程 1 开始,然后为 WriteWithoutSleep() 创建线程 5,但继续在线程 1 上为 ThreadWithSleep() 执行,直到等待延迟任务。此时,线程 1 的代码流入线程 4,在 Main 中的进一步执行现在在线程 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 做几乎相同的事情。


“两者都做同样的事情,它们阻塞了正在执行的线程......” -- AFAIK await task.Delay() 不会阻塞任何线程,如果你愿意,我可以很容易地证明这一点。
Patrick here 证明了 await task.Delay() 不会 阻塞线程。我创建了 100,000 个等待 await task.Delay(1000) 的任务,然后等待它们全部完成。它们都在 1000 毫秒后完成。如果每个 await task.Delay() 阻塞一个线程,我需要 100,000 个线程。每个线程需要 1 MB 的 RAM,所以我需要 100 GB RAM。我的机器只有 4GB RAM。所以我的应用程序不可能创建这么多线程。
Patrick your answer 包含以下文本:“两者都做同样的事情,它们阻塞了正在执行的线程...”。有多少人会读到这句话,并且会明白您不是在字面上谈论阻塞的线程,而是在谈论暂停的执行流?不是很多。正确使用术语很重要,否则人们可能会从您的答案中学到后来不得不忘记的东西。
@TheodorZoulias 感谢您的更正:我已将其更新为:两者都做同样的事情,它们会阻止执行代码...
“分配给 var 的任务在最终等待时会导致线程切换。” -- 这并不总是正确的。例如,在 WinForms 应用程序中,如果您在 Click 事件的异步处理程序中使用 await Task.Delay(1000),则同一线程将在 await 之后运行代码。这是因为在所有 WinForms 应用程序的 UI 线程上都安装了一个特殊的 SynchronizationContext