简体   繁体   中英

Thread.Yield vs. WaitOne

From what I understand, Thread.Yield could be used instead of WaitOne and ManualResetEvent for the purpose of thread signalling.

Although I have not come across a document explaining the exact behavior of WaitOne behind the scenes, I assume that it places the the thread in Wait state and tells the OS scheduler to check if the ManualResetEvent is set every time it is this thread's turn in the queue. If not set, scheduler does not do the context switch and skips to another thread. If set, scheduler puts the thread into running state so the code after WaitOne starts to execute.

On the other hand, using Thread.Yield will cause the context switch no matter the state of the ManualResetEvent and do the check afterwards.

Is my understanding correct? Is there a document that explains the inner workings of WaitOne ?

PS: Here are the sample codes of both versions for demonstration purposes:

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

First off, Hans' comments are completely correct: you are inventing your own spinwait, badly. Don't do that!

That said, your question is not about whether you should re-implement WaitOne, but rather, how WaitOne was implemented by people who did not have it, because it was not yet written . It is entirely reasonable to consider this question; such functions are not magical and were implemented by humans, so how did they do so?

That's an implementation detail and I do not have the source code for the runtime handy; the actual implementation is in a native function called WaitOneNative . However, I can give you a few thoughts.

First, you are right to note that Thread.Yield is a more primitive operation, and therefore it could be used as part of a strategy to build a higher-level operation like WaitOne . But in practice it probably would not be used in the naive manner you describe, for several reasons:

  • Thread.Yield does create a barrier, but it is not 100% obvious from the code that the read of the bool has not been elided, or that the write cannot be delayed. We'd want to make very, very sure that the bool write was being picked up, and that introducing the barrier did not wreck performance.

  • Thread.Yield cedes control to any ready thread on the current processor . What happens if there is no ready thread on the current processor? Maybe ponder that. What keeps this code from heating up an entire CPU? What happens if the thread that is going to do the write is on a different processor? What are all the possible scenarios involving thread starvation, and so on?

  • Consider this scenario: We have a hyperthreaded processor with three threads, Alpha, Bravo and Charlie, and Alpha and Bravo are currently executing in the CPU. Alpha has 10 million nanoseconds left in its quantum, it sees that the flag is false and yields the remainder of its quantum to Charlie. One nanosecond later, Bravo sets the flag. We just took on the entire cost of a context switch and Alpha giving up on the chance to do ten million nanoseconds of work! It would have been better for Alpha to spinwait and burn a few dozen of its ten million nanoseconds rather than take the enormous cost of a context switch. These are the sorts of scenarios you have to take into account when you are designing a new threading primitive . Just getting the control flow right is not good enough; you make a bad decision on a hot path and you can degrade performance by a factor of thousands or millions.

  • And so on.

But wait, it gets worse. Are there more subtle problems that WaitOne has to solve?

Sure. The CLR has invariants that it must maintain. You have to remember that the CLR was invented fundamentally as an extension to COM and the underlying implementation is deeply embedded in the COM world. In particular, all the rules about marshalling still apply. WaitOne effectively puts a thread to sleep, but that can lead to problems with the marshaller. Chris Brumme's article on this is particularly terrifying and elicidating:

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

Read it, see if you can understand all of it. I've read it dozens of times since 2004, and I used to be a professional COM programmer, and I get maybe 80% of it. This is complicated stuff, and if you don't understand it, you can't write a correct implementation of WaitOne that meets the needs of the CLR.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM