何时使用 Task.Delay 而不是 Thread.Sleep 有哪些好的规则?
- 具体来说,是否存在最小值,可以提高一个方法的有效性和效率而不是另一个方法?
- 最后,由于 Task.Delay 在异步/等待状态机上引起上下文切换,因此使用它是否会产生开销?
何时使用 Task.Delay 而不是 Thread.Sleep 有哪些好的规则?
当您想要阻塞当前线程时,请使用Thread.Sleep
。
当您需要逻辑延迟而不阻塞当前线程时,请使用await Task.Delay
。
这些方法的效率不应是首要问题。它们在现实世界中的主要用途是作为I/O操作的重试计时器,这些操作的时间范围在秒而非毫秒级别。
Thread.Sleep
将阻塞当前线程,导致上下文切换。如果您正在使用线程池,则这也可能导致分配新线程。这两个操作都非常耗费资源,而 Task.Delay
等协作式多任务提供了避免所有这些开销、最大化吞吐量、允许取消并提供更清洁的代码的设计。 - Corillianonesi: 我会在同步方法中使用
Thread.Sleep来等待。然而,在生产代码中我从未这样做过;根据我的经验,我所见过的每个
Thread.Sleep`都表明存在某种需要得到适当修复的设计问题。 - Stephen ClearyTask.Delay
和 Thread.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
则会释放线程以便进行其他工作。
Thread.Sleep()
会消耗整个可以在其他地方使用的线程。如果使用 Thread.Sleep() 运行许多任务,则有很高的可能性耗尽所有线程池线程并严重影响性能。 - Ryanasync
方法。在线程池线程中运行Thread.Sleep()
基本上是个坏主意,但总体来说并不是一个坏主意。毕竟,在采用(虽然不被鼓励的)Task.Factory.StartNew()
路线时,有TaskCreationOptions.LongRunning
选项。 - sunsideTasl.Delay
的文档说明是使用系统计时器。由于“系统时钟”以恒定速率“滴答”运行,所以系统计时器的滴答速度约为16毫秒,你请求的任何延迟都将舍入为系统时钟的滴答数,加上到第一个滴答的时间偏移量。请参阅 Task.Delay
的 msdn 文档,向下滚动至备注。 - Dorus我想要补充一些内容。
实际上,Task.Delay
是一种基于计时器的等待机制。如果你查看源代码,你会发现引用了一个 Timer
类负责延迟。另一方面,Thread.Sleep
会让当前线程睡眠,这样你只是在阻塞并浪费一个线程。在异步编程模型中,如果你想要延迟后执行某些操作(继续),应该始终使用 Task.Delay()
。
如果当前线程被终止且正在执行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
异常(并将其抑制),但是我的线程仍然存在。真棒! :) - AlexMelwDelayed
可能更适合作为Task.Delay
的名称 - 因为它不会延迟现有任务,而是创建一个新的“延迟”任务,可以等待并且会使当前任务体挂起。它本质上就像一个没有回调/任务体的定时器。Thread.Sleep(),该线程将被阻塞,即在所请求的时间内将退出并不会处理来自队列的任何异步消息。
在.NET中,有两种主要的并行处理方法,旧的基于线程、线程池等,新的则基于任务、async/await 和TPL。通常情况下,不应该混合使用这两个不同的 API 。
值得一提的是,Thread.Sleep(1) 会更快地触发 GC。
这纯粹基于我和团队成员的观察。假设您有一个服务,每个特定请求都会创建新任务(大约200-300个正在进行),并且此任务在流程中包含许多弱引用。该任务像状态机一样工作,因此我们在状态更改时触发 Thread.Sleep(1),通过这样做,我们成功地优化了应用程序中的内存利用率 - 正如我之前所说 - 这将使 GC 更快地触发。在低内存消耗服务(<1GB)中,这没有太大区别。
我和同事就这个问题进行了长时间的争论,他向我证明了除目前最佳答案所示外还有显著的区别。如果你 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 Zouliasawait 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几乎做相同的事情。
await task.Delay()
和阻塞执行线程的作用是一样的,但它并不会阻塞任何线程。如果您愿意,我可以轻松证明这一点。 - Theodor Zouliasawait task.Delay()
不会 阻塞线程。我创建了 100,000 个任务,它们都在等待 await task.Delay(1000)
,然后等待它们全部完成。它们在 1000 毫秒后全部完成。如果每个 await task.Delay()
会阻塞一个线程,那我就需要 100,000 个线程。每个线程需要 1MB 的 RAM,所以我需要 100 GB 的 RAM。可是我的机器只有 4GB 的 RAM。所以我的程序不可能创建那么多线程。 - Theodor Zouliasawait Task.Delay(1000)
,则同一线程将在“await”之后运行代码。这是因为在所有WinForms应用程序的UI线程上安装了特殊的“同步上下文”。 - Theodor Zoulias我的观点是,Task.Delay()
是异步的。它不会阻塞当前线程。您仍然可以在当前线程中执行其他操作。它返回一个 Task 类型(Thread.Sleep()
不返回任何内容)。您可以在另一个耗时较长的过程后稍后检查此任务是否已完成(使用 Task.IsCompleted
属性)。
Thread.Sleep()
没有返回类型。它是同步的。在线程中,除了等待延迟完成,您实际上不能做任何事情。
至于实际应用,我已经编程 15 年了。我从未在生产代码中使用过 Thread.Sleep()
。我找不到任何用例。可能是因为我主要开发 Web 应用程序。