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


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. Anonymous 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. Anonymous 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. Anonymous says:

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

  4. Anonymous 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. Anonymous says:

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

  6. Anonymous 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. Anonymous 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. Anonymous says:

    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 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. Anonymous 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. Anonymous 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. Anonymous 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. Anonymous 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. Anonymous says:

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

Comments are closed.