何时使用 Task.Delay 与 Thread.Sleep 是否有良好的规则?
具体来说,是否有一个最小值来规定一个比另一个有效/高效?
最后,由于 Task.Delay 导致异步/等待状态机上的上下文切换,使用它是否有开销?
当您想要阻止当前线程时使用 Thread.Sleep
。
如果您想要一个逻辑延迟而不阻塞当前线程,请使用 Task.Delay
。
效率不应该是这些方法的首要问题。它们在现实世界中的主要用途是作为 I/O 操作的重试计时器,其数量级为秒而不是毫秒。
Task.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 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()
不必要地阻塞单个线程会消耗整个线程,否则该线程可以在其他地方使用。如果使用 Thread.Sleep() 运行许多任务,则很有可能耗尽所有线程池线程并严重影响性能。
async
方法时,我错过了异步代码的概念。在线程池线程中运行 Thread.Sleep()
基本上只是一个坏主意,一般来说不是一个坏主意。毕竟,走(尽管不鼓励)Task.Factory.StartNew()
路线时会有 TaskCreationOptions.LongRunning
。
Thread.Sleep
用于非常低的值,例如 Thread.Sleep(50)
(用于某些硬件通信)。
Tasl.Delay
使用系统计时器。由于“系统时钟以恒定速率“滴答”,系统计时器的滴答速度约为 16 毫秒,您请求的任何延迟都将四舍五入为系统时钟的滴答数,偏移到第一个时钟的时间打钩。请参阅 Task.Delay
docs.microsoft.com/en-us/dotnet/api/… 上的 msdn 文档并向下滚动到备注。
我想补充一点。实际上,Task.Delay
是一个基于计时器的等待机制。如果您查看 source,您会发现对导致延迟的 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
(并抑制它)但我的线程仍然存在。整洁的! :)
Delayed
将是 Task.Delay
的更好名称 - 因为它不会延迟现有任务,而是创建一个新的“延迟”任务,另一方面,它可以等待并可能导致当前任务的主体暂停。它本质上是一个计时器,但没有回调/正文。
等待延迟任务会在异步消息队列中创建一个新项目,并且不会阻塞任何线程。如果有任何其他任务,调用 await 的同一线程将继续处理其他任务,并在超时后(或队列中的前面项目完成时)返回到等待点。后台任务使用线程 - 可以在单个线程中安排和执行许多任务。另一方面,如果您碰巧调用了 Thread.Sleep()
,线程将阻塞,即它会在请求的时间内停止运行,并且不会处理来自队列的任何异步消息。
在 .NET 中有两种主要的并行方法。旧的有线程、线程池等。新的基于任务、异步/等待、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();
无关。您可以通过使用 Sleep
来证明这一点。另请注意,当 DoSomething2 从 Task.Delay “返回”时,它在不同的线程上运行。
这个东西比我认为的要聪明得多,因为我相信 await
刚刚开始了一个新线程来做一些事情。我仍然没有假装理解这一切,但上面的反直觉结果表明,除了启动线程来运行代码之外,还有更多的事情要做。
我的意见,
Task.Delay()
是异步的。它不会阻塞当前线程。您仍然可以在当前线程中执行其他操作。它返回一个 Task 返回类型(Thread.Sleep()
不返回任何东西)。您可以在另一个耗时的过程之后检查此任务是否已完成(使用 Task.IsCompleted
属性)。
Thread.Sleep()
没有返回类型。是同步的。在线程中,除了等待延迟完成之外,您实际上无法做任何事情。
至于现实生活中的使用,我已经编程了 15 年。我从未在生产代码中使用过 Thread.Sleep()
。我找不到任何用例。也许那是因为我主要从事 Web 应用程序开发。
在异步程序中,两者的区别
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 做几乎相同的事情。
await task.Delay()
不会阻塞任何线程,如果你愿意,我可以很容易地证明这一点。
await task.Delay()
不会 阻塞线程。我创建了 100,000 个等待 await task.Delay(1000)
的任务,然后等待它们全部完成。它们都在 1000 毫秒后完成。如果每个 await task.Delay()
阻塞一个线程,我需要 100,000 个线程。每个线程需要 1 MB 的 RAM,所以我需要 100 GB RAM。我的机器只有 4GB RAM。所以我的应用程序不可能创建这么多线程。
Click
事件的异步处理程序中使用 await Task.Delay(1000)
,则同一线程将在 await
之后运行代码。这是因为在所有 WinForms 应用程序的 UI 线程上都安装了一个特殊的 SynchronizationContext
。
不定期副业成功案例分享
Thread.Sleep
将阻塞导致上下文切换的当前线程。如果您使用的是线程池,这也可能导致分配一个新线程。这两个操作都非常繁重,而Task.Delay
等提供的协作多任务处理旨在避免所有这些开销、最大化吞吐量、允许取消并提供更简洁的代码。onesi: I would use
Thread.Sleep` 在同步方法中等待。但是,我从不在生产代码中这样做;根据我的经验,我见过的每一个Thread.Sleep
都表明存在某种需要适当修复的设计问题。