19

我想知道调用 Thread.Sleep(1) 和调用 SwitchToThread 之间的实际区别是什么(如果我们忽略它当前没有被 BCL 公开)。

Joe Duffy 在他的帖子中提到:

“kernel32!SwitchToThread API 不会出现 Sleep(0) 和 Sleep(1) 会出现的问题。” (关于调度程序的行为)

为什么 Sleep 的行为不像 SwitchToThread?为什么存在这种差异化,它有什么用?(如果有的话..)

4

2 回答 2

25

有两个区别。第一个在SwitchToThread的 MSDN 文档中提到:

执行的产量仅限于调用线程的处理器。操作系统不会将执行切换到另一个处理器,即使该处理器处于空闲状态或正在运行优先级较低的线程。

Sleep(0) 也将允许其他处理器上的线程运行。

SwitchToThread 也只屈服于单个线程调度上下文。另一方面,睡眠有多个等待的条件。SleepEx的文档详细说明了这一点:

* An I/O completion callback function is called
* An asynchronous procedure call (APC) is queued to the thread.
* The time-out interval elapses

这将产生多个线程。

一般来说,Sleep(0) 更有可能产生时间片,并且总是让给操作系统,即使没有其他线程在等待。这就是为什么在循环中添加 Sleep(0) 会使处理器使用率从 100%(每个内核)降低到在许多情况下接近 0%。SwitchToThread 不会,除非另一个线程正在等待时间片。

于 2009-09-05T18:25:58.027 回答
7

SwitchToThread() 是 Sleep(0) 的“更智能”版本。它没有很好的记录,但据我了解,它的工作方式如下:

  1. 当有其他线程处于该ready状态时(即,想要运行的线程比可用的逻辑处理器多)并且这些线程的优先级与调用 SwitchToThread() 的线程相同或更高,它的行为方式与睡眠相同(0) - 即将逻辑处理器让给这些线程之一,代价高昂的上下文切换;
  2. 当线程处于较低优先级ready的状态时,它就退出,即调用 SwitchToThread() 的线程继续执行,无需任何上下文切换或 3 到 0 转换的费用​​(它不会离开用户模式)-这与 Sleep(0) 的行为方式相反,它总是将控制权交给最低优先级的线程
  3. 当状态中没有线程时ready, SwitchToThread() 也会像 Sleep(0) 一样退出- 因此,如果您在循环中执行此操作,您只会获得当前逻辑处理器的 100% 负载,即消耗功率。

Sleep(1) 与 Sleep(0) 相同,但后面有 1 毫秒的延迟。这 1 毫秒的延迟释放了逻辑处理器并且不会消耗任何功率。相反,SwitchToThread 永远不会遇到任何延迟。

所以最好将 SwitchToThread 与 Sleep(0) 进行比较,而不是与 Sleep(1) 进行比较,因为 Sleep(1) 与 Sleep(0) + 延迟 1 毫秒相同。

我从“Intel 64 and IA-32 Architectures Optimization Reference Manual”和“Intel 64 and IA-32 Architectures Software Developer's Manual”中借用了一些关于这个问题的想法,它们倾向于调用一些pauseCPU 指令(也可作为内在函数)如果您的等待时间很短,请使用 SwitchToThread() 或 Sleep(0)。请注意 SwitchToThread() 或 Sleep(0) 几乎是立即的,而 Sleep(1) 至少持续一毫秒。

还应考虑以下因素:

  • 每次调用 Sleep() 或 SwitchToThread() 都会经历上下文切换的昂贵成本,可能是10000+ 个周期
  • 它还承受从 ring 3 到 ring 0 转换的成本,可能是1000+ 个周期
  • 如果没有线程处于该状态,SwitchToThread() 或 Sleep(0) 可能没有用ready,但无论是否有其他线程处于“就绪”状态,Sleep(1) 都会等待至少一毫秒。

如果您的等待循环往往很短,请考虑先执行一些pauseCPU 指令。通过在 SwitchToThread() 或 Sleep() 调用之前使用一些 CPU 指令减慢“旋转等待” pause,多线程软件获得:

  • 通过促进等待任务从繁忙的等待中更轻松地获取资源来提高性能。
  • 通过在旋转时使用更少的管道部分来节省电力。
  • 消除了由 SwitchToThread() 或 Sleep(0) 或 Sleep(1) 调用的开销引起的绝大多数不必要的执行指令。

但是,如果您要调用运行至少一毫秒的 Sleep(1),这在 CPU 周期方面非常长,那么您期望的等待周期会非常长,因此pause在这种情况下指令将是徒劳的.

当等待循环预计持续很长时间时,最好通过调用操作系统同步 API 函数之一来让步给操作系统,例如 Windows 操作系统上的 WaitForSingleObject,而不是 SwitchToThread() 或 Sleep(0) 或 Sleep( 1),因为它们在长时间等待时非常浪费。此外,Sleep(1) 非常慢,并且诸如 WaitForSingleObject 或 EnterCriticalSection 之类的 OS 同步函数将反应更快,并且它们对资源更友好。

我的结论:最好不要使用 Sleep(0) 或 Sleep(1) 或 SwitchToThread()。不惜一切代价避免“旋转等待”循环。使用WaitForMultipleObjects()、SetEvent()等高级同步函数——从性能、效率和省电的角度来看,它们是最好的。尽管它们还遭受昂贵的上下文切换和 ring 3 到 ring 0 的转换,但与您在使用 Sleep() 或 SwitchToThread() 的“自旋等待”循环中花费的费用相比,这些费用并不常见,而且非常合理。

在支持 HT 技术的处理器上,自旋等待循环会消耗处理器执行带宽的很大一部分。一个执行自旋等待循环的逻辑处理器会严重影响另一个逻辑处理器的性能。这就是为什么有时禁用 HT 可能会提高性能。

持续轮询设备或文件或其他数据源的状态更改可能会导致计算机消耗更多电量,给内存和系统总线带来压力,并提供不必要的页面错误(使用 Windows 中的任务管理器查看哪个应用程序在空闲时产生大多数页面错误 - 这些是最低效的应用程序,因为它们使用“轮询”)。尽可能减少轮询并使用事件驱动的方式编写应用程序。这是我强烈推荐的最佳实践。您的应用程序应该一直处于睡眠状态,等待预先设置的多个事件。事件驱动应用程序的一个很好的例子是 Linux 下的 Nginx。以轮询电源更改为例。如果操作系统为各种设备状态更改提供通知服务(甚至是 WM_ 消息),例如将电源从 AC 转换为电池,请使用这些通知服务而不是轮询设备状态更改。这种方法减少了代码轮询电源状态的开销,因为代码可以在状态更改发生时异步获取通知。

与某些人所写的相反,Sleep(0) 不会将 CPU 消耗减少到接近零。它将执行释放给处于“就绪”状态的其他线程,但如果没有这样的线程,它只会浪费数千个 CPU 周期并消耗当前线程 100% 的 CPU 周期,stackoverflow 成员也证明了这一点- 和我也刚刚重新检查了一遍 - Sleep(0) 循环在 Windows 10 64 位上消耗了当前线程的 100% CPU。

于 2017-07-02T22:19:01.560 回答