Thread.Yield 与 WaitOne

Posted

技术标签:

【中文标题】Thread.Yield 与 WaitOne【英文标题】:Thread.Yield vs. WaitOne 【发布时间】:2019-03-31 09:14:09 【问题描述】:

据我了解,Thread.Yield 可以代替WaitOneManualResetEvent 用于线程信号。

虽然我没有遇到解释 WaitOne 在幕后的确切行为的文档,但我假设它将线程置于等待状态并告诉操作系统调度程序检查是否每次都设置了 ManualResetEvent轮到这个线程进入队列了。如果未设置,调度程序不会进行上下文切换并跳到另一个线程。如果设置,调度程序会将线程置于运行状态,以便WaitOne 之后的代码开始执行。

另一方面,无论ManualResetEvent 的状态如何,使用Thread.Yield 都会导致上下文切换,然后进行检查。

我的理解正确吗?有没有解释WaitOne内部工作原理的文档?

P.S:以下是两个版本的示例代码,用于演示目的:

var signal = new ManualResetEvent(false);
new Thread(() =>

    Console.WriteLine("Waiting for signal...");
    signal.WaitOne();
    signal.Dispose();
    Console.WriteLine("Got signal!");
).Start();
Thread.Sleep(2000);
signal.Set(); // "Open" the signal

bool signal = false;
new Thread(() =>

    Debug.WriteLine("Waiting for signal...");
    while(signal == false)
    
        Thread.Yield();
    
    Debug.WriteLine("Got signal!");
).Start();
Thread.Sleep(2000);
signal = true; ; // "Open" the signal

【问题讨论】:

referencesource.microsoft.com 很棒。 WaitOne WaitOne 是一个半量,它允许一个线程等待另一个线程完成。 msdn 上的示例比您发布的代码更好地了解它的实际工作原理。 msdn.microsoft.com/en-us/library/aa332439(v=vs.71).aspx 他们没有任何关系。 Yield() 与信令或同步无关,也不保证线程上下文切换。它也不能将bool 变成同步原语。您将使用它来实现您自己的 SpinWait()。好吧,不要那样做,它已经完成了。 @HansPassant MS 文档说:“导致调用线程让步给另一个准备好在当前处理器上运行的线程。”对于 Thread.Yield 好吧,猜猜当没有其他线程准备好运行时会发生什么。正常情况。 【参考方案1】:

首先,Hans 的 cmets 是完全正确的:您正在发明自己的 spinwait,非常糟糕。不要那样做!

也就是说,您的问题不在于您是否应该重新实现 WaitOne,而是没有它的人如何实现 WaitOne,因为它尚未编写。考虑这个问题是完全合理的;这样的功能并不神奇,是人为实现的,那它们是怎么做到的呢?

这是一个实现细节,我手边没有运行时的源代码;实际实现是在一个名为WaitOneNative 的本机函数中。不过,我可以给你一些想法。

首先,您注意到Thread.Yield 是一个更原始的操作是正确的,因此它可以用作构建更高级别操作(如WaitOne)的策略的一部分。但在实践中,它可能不会以您描述的幼稚方式使用,原因如下:

Thread.Yield 确实创建了一个屏障,但从代码中并没有 100% 明显看出 bool 的读取没有被忽略,或者写入不能延迟。我们想要非常非常确定 bool 写入被拾取,并且引入屏障不会破坏性能。

Thread.Yield 将控制权让给当前处理器上的任何就绪线程。如果当前处理器上没有就绪线程会发生什么?或许可以考虑一下。是什么让这段代码不会加热整个 CPU?如果要进行写入的线程在不同的处理器上会发生什么?涉及线程饥饿等的所有可能场景有哪些?

考虑这种情况:我们有一个具有三个线程的超线程处理器,Alpha、Bravo 和 Charlie,Alpha 和 Bravo 当前在 CPU 中执行。 Alpha 的量子还剩 1000 万纳秒,它发现标志是假的,并将剩余的量子交给查理。一纳秒后,Bravo 设置了标志。我们刚刚承担了上下文切换的全部成本,而 Alpha 放弃了做 1000 万纳秒工作的机会!对于 Alpha 来说,旋转等待并烧掉它一千万纳秒中的几十个而不是承担上下文切换的巨大成本会更好。 这些是您在设计新的线程原语时必须考虑的各种场景。仅仅获得正确的控制流是不够的;您在热门路径上做出了错误的决定,您可能会降低数千或数百万倍的性能。

等等。

但是等等,情况会变得更糟。 WaitOne 是否还有更微妙的问题需要解决?

当然。 CLR 具有必须维护的不变量。您必须记住,CLR 基本上是作为 COM 的扩展而发明的,其底层实现深深嵌入在 COM 世界中。特别是,所有关于编组的规则仍然适用。 WaitOne 有效地使线程进入睡眠状态,但这可能会导致编组器出现问题。 Chris Brumme 关于这方面的文章特别令人恐惧和阐明:

https://blogs.msdn.microsoft.com/cbrumme/2004/02/02/apartments-and-pumping-in-the-clr/

阅读它,看看你是否能全部理解。自 2004 年以来,我已经阅读了数十次,我曾经是一名专业的 COM 程序员,大概能读到 80% 的内容。这是一个复杂的东西,如果你不理解它,你就无法编写出满足CLR需求的WaitOne的正确实现。

【讨论】:

非常感谢您的详细解释。我试图将 WaitOne 与 Thread.Yield 进行对比,以了解前者的缺点以及后者如何解决这些问题。您还提供了一些非常重要的观点。 阅读您提到的第三个要点,除了您提到的许多其他因素,我是否可以得出结论认为我在问题中的假设是正确的? ManualResetEvent 是一个由 OS 调度程序监视的特殊结构,因此它可以跳过线程,从而消除了对上下文切换的需要,而使用 Thread.Yield,只能在调度程序加载线程上下文后进行此检查。在这个意义上它是如何工作的? @JohnL.:我不知道详细程度;我从来没有接近过在那个低级别实现组件,并且在可能的情况下我将它们视为黑匣子。您必须检查源代码。 顺便说一句,我认为这是任何感兴趣的人的本机 WaitOne 实现的链接:github.com/dotnet/coreclr/blob/master/src/vm/…。不过,我无法找到有关这种特定行为的线索。 @JohnL.:我认为您真正想要查看的方法是实际执行逻辑的等待辅助方法。请注意,正如我所提到的,它需要确定是否存在要抽水的队列。另外,我完全忘了提到显然 CLR 线程在等待时仍需要适当地与调试器合作,所以逻辑也在这里。 github.com/dotnet/coreclr/blob/…

以上是关于Thread.Yield 与 WaitOne的主要内容,如果未能解决你的问题,请参考以下文章

Thread.yield()方法

Thread.sleep( ) vs Thread.yield( )

Thread 中yield(), join()

Thread.sleep() 和 Thread.yield() 区别

C++ std::this_thread::yield()函数(线程抑制线程让步让出时间片)volatile

Thread.yield()的简单理解