Consequences of the scheduling algorithm: Sleeping doesn’t always help

Date:October 4, 2005 / year-entry #290
Tags:code
Orig Link:https://blogs.msdn.microsoft.com/oldnewthing/20051004-09/?p=33923
Comments:    20
Summary:More often I see the reverse of the "Low priority threads can run even when higher priority threads are running" problem. Namely, people who think that Sleep(0) is a clean way to yield CPU. For example, they might have run out of things to do and merely wish to wait for another thread to produce...

More often I see the reverse of the "Low priority threads can run even when higher priority threads are running" problem. Namely, people who think that Sleep(0) is a clean way to yield CPU. For example, they might have run out of things to do and merely wish to wait for another thread to produce some work.

Recall that the scheduler looks for the highest priority runnable thread, and if there is a tie, all the candidates share CPU roughly equally. A thread can call Sleep(0) to relinquish its quantum, thereby reducing its share of the CPU. Note, however, that this does not guarantee that other threads will run.

If there is a unique runnable thread with the highest priority, it can call Sleep(0) until the cows come home, and it will nevertheless not relinquish CPU. That's because sleeping for zero milliseconds release the quantum but leaves the thread runnable. And since it is the only runnable thread with the highest priority, it immediately gets the CPU back. Sleeping for zero milliseconds is like going to back of the line. If there's nobody else in line, you didn't actually yield to anyone!

Therefore, if you use Sleep(0) as an ineffective yield, you will never allow lower priority threads to run. This means that various background activities (such as indexing) never get anywhere since your program is hogging all the CPU. What's more, the fact that your program never actually releases the CPU means that the computer will never go into a low-power state. Laptops will drain their batteries faster and run hotter. Terminal Servers will spin their CPU endlessly.

The best thing to do is to wait on a proper synchronization object so that your thread goes to sleep until there is work to do. If you can't do that for some reason, at least sleep for a nonzero amount of time. That way, for that brief moment, your thread is not runnable and other threads—including lower-priority threads—get a chance to run. (This will also reduce power consumption somewhat, though not as much as waiting on a proper synchronization object.)


Comments (20)
  1. Travis Owens says:

    Interesting fact to know, but I can’t imagine why anybody would ever think Sleep(0) was a good idea in the first place.

    It seems common sense one would read this as a pointless roadblock that just eats cpu because we’re executing a step that doesn’t actually do anything, so no sleep’ing is going on. A microscopic step, but still a step.

  2. Richard says:

    I know I’ve done this… as a pure yeild, in a reader-writer lock for a low contention scenarios (readers:writers ~ 1000000:1) so wanted the optimal reader entry (no writer) being no more than an interloced operation. But this meant there was some signigificant cost on a writer entry (spin on InterlockedCompareExchange/Sleep(0)).

    TANSTAAFL, if used where there were a lot of writers, you would see the cost.

  3. denis says:

    What about SwitchToThread(), is that any different (or better) than Sleep(0)?

  4. John says:

    Cool! I must confess to being one of those who has always believed that calling sleep(0) was a good way to tell the OS "Well, I’m done – you can allocate my remaining CPU time to someone else".

    sigh.

  5. X says:

    Will Sleep(1) work? Won’t that it give a whole quantom to a different thread?

  6. John says:

    Invariably, I end up using sleep instead of a "proper synchronization object" when work with files. For instance, a log file used by multiple processes. I’ve always wanted some form of a "file changed" notification and "file is no longer locked" notification.

    Is there a way to do this without polling?

  7. Joseph Bubba says:

    A while back I had to modify a piece of code that started two threads (A and B). The first thread (A) would do some initialization work and then lower it’s priority under tha assumption that the second thread (B) of normal priority would come in immediately thereafter and finish up the initialization. In some cases though, it seemed like thread A kept running for a short while before thread B would come in and finish up the initialization. It’s almost like the call to SetThreadPriority(THREAD_PRIORITY_BELOW_NORMAL) did not trigger the scheduler to reschedule the threads immediately. Note that this scenario was exhibited on a single processor machine.

    Do you have any insight on specifically when the scheduler reschedules threads? Maybe this is where a Sleep(0) might have helped?

    In the end, I added some specific synchronization code in to fix the problem and force the proper order of operations.

  8. The File Change Notification APIs don’t help here. You pretty much have to just do back off. That said, you would never "back off" using a CPU-bound spin, so Sleep(0) and the quantum-ending effects would not be noticable.

    There’s no good solution for this one. Asynchronous/cancellable file creation/opening might help but even at the NT layer I don’t think that there’s a way to block on waiting for handles with incompatible sharing flags to be closed.

  9. Anonymous Coward says:

    The File Change Notification APIs help a lot here – in fact, they’re exactly what is required. You ask for an object to be signalled when the file changes, and wait for the object to be signalled. This allows any other threads/processes to run and modify the file, and then as soon as your thread is eligable to run, it will stop waiting. There’s no screwing around with sleeping or "backing off" – you just wait without using up CPU time.

    To quote from the docs referenecd just above: "This function also returns a handle that can be waited on by using the wait functions" – which includes our friend WaitForMultipleObjectsEx.

    If you don’t know and love WaitForMultipleObjectsEx and its friends in detail you’re never going to be able to write high quality threaded applications on Windows, and you’ll have trouble writing software that isn’t a CPU hog causing problems for every other application on the system.

  10. Jonathan says:

    I think Sleep(0) is a holdover from cooperative multi-tasking days (Windows 3.x), where the scheduler couldn’t pre-empt your thread, so you had to perform some specific yielding operation. Sleep(0) sure looks like one…

  11. Keith Nicholas says:

    When trying to do realtime stuff this is most annoying… Sleep(0) will yield to higher or equal priority threads, Sleep(1) will go away for up to 15ms, which sucks.

  12. Moz says:

    File Change Notification APIs only help if the other file user is guaranteed to change the file before releasing it. If it ever releases the file unchanged you’re back to polling. But hopefully on a sleep() longer than 0.

  13. vince says:

    "When trying to do realtime stuff this is most annoying… Sleep(0) will yield to higher or equal priority threads, Sleep(1) will go away for up to 15ms, which sucks."

    Agree 100%. This problem shows up in game development all the time.

  14. Frederik Slijkerman says:

    "It’s almost like the call to SetThreadPriority(THREAD_PRIORITY_BELOW_NORMAL) did not trigger the scheduler to reschedule the threads immediately."

    That’s exactly what I found, too. A Sleep(0) after the SetThreadPriority call seemed to help.

  15. Ian Ringrose says:

    I the past I have used sleep(0) to mean “This is a good time to switch threads” E.g. by giving up some of my CPU time at the top of the loop I am less likely to be switched out while I have a lot of locks etc in the middle of the loop

  16. Bryan says:

    And then there’s YieldProcessor(), which (I read in MSDN) you need when doing spin-locks on hyperthreaded systems.

Comments are closed.


*DISCLAIMER: I DO NOT OWN THIS CONTENT. If you are the owner and would like it removed, please contact me. The content herein is an archived reproduction of entries from Raymond Chen's "Old New Thing" Blog (most recent link is here). It may have slight formatting modifications for consistency and to improve readability.

WHY DID I DUPLICATE THIS CONTENT HERE? Let me first say this site has never had anything to sell and has never shown ads of any kind. I have nothing monetarily to gain by duplicating content here. Because I had made my own local copy of this content throughout the years, for ease of using tools like grep, I decided to put it online after I discovered some of the original content previously and publicly available, had disappeared approximately early to mid 2019. At the same time, I present the content in an easily accessible theme-agnostic way.

The information provided by Raymond's blog is, for all practical purposes, more authoritative on Windows Development than Microsoft's own MSDN documentation and should be considered supplemental reading to that documentation. The wealth of missing details provided by this blog that Microsoft could not or did not document about Windows over the years is vital enough, many would agree an online "backup" of these details is a necessary endeavor. Specifics include:

<-- Back to Old New Thing Archive Index