据我了解,出于线程信令的目的,可以使用Thread.Yield
代替WaitOne
和ManualResetEvent
。
尽管我还没有找到文档来说明WaitOne
在幕后的确切行为,但我认为它将线程置于Wait状态并告诉OS调度程序检查ManualResetEvent
每次轮到该线程进入队列时设置。如果未设置,则调度程序不执行上下文切换,而是跳到另一个线程。如果已设置,则调度程序将线程置于运行状态,以便WaitOne
之后的代码开始执行。
另一方面,无论Thread.Yield
的状态如何,使用ManualResetEvent
都会导致上下文切换,然后进行检查。
我的理解正确吗?是否有文档解释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
答案 0 :(得分:3)
首先,汉斯的评论是完全正确的:您正在发明自己的旋转等待,非常糟糕。不要那样做!
也就是说,您的问题不是关于是否应该重新实现WaitOne,而是 WaitOne是如何由没有它的人实现的,因为它尚未编写。考虑这个问题是完全合理的;这些功能不是神奇的,而是由人类实现的,那么它们是怎么做到的呢?
这是一个实现细节,我没有方便的运行时源代码;实际的实现是在名为WaitOneNative
的本机函数中进行的。但是,我可以给您一些想法。
首先,您正确地注意到Thread.Yield
是更原始的操作,因此可以将其用作构建像WaitOne
这样的更高级别操作的策略的一部分。但实际上,由于以下几个原因,它可能不会以您描述的幼稚方式使用:
Thread.Yield
确实创建了一个障碍,但是从代码中并不能100%清晰地知道bool的读取没有被消除,或者写入不能被延迟。我们要非常非常确定地确定bool的写法,并且引入障碍不会破坏性能。
Thread.Yield
将控制权移交给当前处理器上的任何就绪线程 。如果当前处理器上没有就绪线程,该怎么办?也许考虑一下。是什么使该代码无法加热整个CPU?如果要执行写操作的线程在不同的处理器上会发生什么?涉及线程匮乏等所有可能的情况是什么?
请考虑以下情形:我们有一个具有三个线程的超线程处理器,Alpha,Bravo和Charlie,并且Alpha和Bravo当前正在CPU中执行。 Alpha的量子剩余时间为1000万纳秒,它发现该标志是错误的,并将其剩余的量子提供给Charlie。一纳秒后,Bravo设置了标志。我们只承担了上下文切换的全部费用,而Alpha放弃了进行一千万纳秒工作的机会!对于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
的正确实现。