47

Joe Duffy (author of Concurrent Programming on Windows) writes in this blog article that Thread.Sleep(1) is preferred over Thread.Sleep(0) because it will suspend for same and lower priority threads, not just equal priority threads as for Thread.Sleep(0).

The .NET version of MSDN says that Thread.Sleep(0) is special, it will suspend this thread and allow other waiting threads to execute. But it says nothing about Thread.Sleep(1) (for any .NET version).

So, is Thread.Sleep(1) actually doing anything special?

Background:

I'm refreshing my knowledge of concurrent programming. I wrote some C# code to visibly show that pre/post increments and decrements are non-atomic and therefore not thread-safe.

To avoid needing to create hundreds of threads I place a Thread.Sleep(0) after incrementing a shared variable to force the scheduler to run another thread. This regular swapping of threads makes the non-atomic nature of pre/post increment/decrement more obvious.

Thread.Sleep(0) appears to causes no additional delay, as expected. However if I change this to Thread.Sleep(1), it appears to revert to normal sleep behaviour (eg. I get roughly a minimum of 1ms delay).

This would mean that while Thread.Sleep(1) may be preferred, any code that uses it in a loop would run much slower.

This SO question "Could someone explain this interesting behaviour with Sleep(1)?" is sort of relevant, but it is C++ focused and just repeats the guidance in Joe Duffy's blog article.

Here's my code for anyone interested (copied from LinqPad, so you may need to add a class around it):

int x = 0;

void Main()
{
    List<Thread> threadList=new List<Thread>();
    Stopwatch sw=new Stopwatch();

    for(int i=0; i<20; i++)
    {
        threadList.Add(new Thread(Go)); 
        threadList[i].Priority=ThreadPriority.Lowest;
    }

    sw.Start();

    foreach (Thread thread in threadList)
    {
        thread.Start();
    } 


    foreach (Thread thread in threadList)
    {
        thread.Join();
    } 

    sw.Stop();
    Console.WriteLine(sw.ElapsedMilliseconds);

    Thread.Sleep(200);
    Console.WriteLine(x);
}

void Go()
{
    for(int i=0;i<10000;i++)
    {
        x++;
        Thread.Sleep(0);
    }
}
Community
  • 1
  • 1
Ash
  • 60,973
  • 31
  • 151
  • 169
  • 3
    Thread.Sleep(1) isn't special in the sense that Sleep(2) or Sleep(15) will do anything fundamentally different. Sleep(1) is just the lowest value higher than Sleep(0), which *is* special (or used to be). – Joren May 16 '13 at 12:06

1 Answers1

59

You no longer need to use Sleep(1) instead of Sleep(0) because Microsoft changed the implementation of the Windows API Sleep().

From the MSDN documentation for Sleep(), this is what happens now with Sleep(0):

A value of zero causes the thread to relinquish the remainder of its time slice to any other thread that is ready to run. If there are no other threads ready to run, the function returns immediately, and the thread continues execution.

This is what used to happen in Windows XP:

A value of zero causes the thread to relinquish the remainder of its time slice to any other thread of equal priority that is ready to run. If there are no other threads of equal priority ready to run, the function returns immediately, and the thread continues execution. This behavior changed starting with Windows Server 2003.

Note the difference between "any other thread" and "any other thread of equal priority".

The only reason that Joe Duffy suggests using Sleep(1) rather than Sleep(0) is because it is the shortest Sleep() value that will prevent the Sleep() from returning immediately if there are no other threads of equal priority ready to run, when running on Windows XP.

You don't need to worry about this for OS versions after Windows Server 2003 because of the change in behaviour of Sleep().

I draw your attention to this part of Joe's blog:

And even though there's an explicit Sleep in there, issuing it doesn't allow the producer to be scheduled because it's at a lower priority.

In XP, lower priority threads would be starved even if the main thread (of higher priority) did Sleep(0). Post-XP, this will no longer happen because Sleep(0) will allow the lower priority threads to run.

Matthew Watson
  • 104,400
  • 10
  • 158
  • 276
  • Good to know, thanks. I take it you're not referring to the **.NET** MSDN documentation? (the page I linked to above). I can't find that important text from your quote. – Ash May 16 '13 at 10:13
  • Sorry I'll add a link to the MSDN docs for Windows API Sleep() ... and done. – Matthew Watson May 16 '13 at 10:15
  • 1
    Great!, that'll teach me for relying on MSDN .NET docs and an article from 2006. Accepted answer. – Ash May 16 '13 at 10:19
  • 1
    @AshleyHenderson The only other thing I should say is that I have offered no direct evidence that `Thread.Sleep()` does actually call the Windows API Sleep(). I'm sure it does, but Reflector just shows `SleepInternal()` being called, so I'm unable to prove it. – Matthew Watson May 16 '13 at 10:25
  • Same here regarding Reflector/SleepInternal, but I agree it's very likely. I'm glad they've changed things because having to remember what effects different integer values passed to Sleep have does not make for clear code. – Ash May 16 '13 at 10:34
  • Maybe this is a whole new question but I seem to recall that the minimum resolution of Sleep() was about 15 to 16 ms on Windows. But Sleep(1) seems to be pretty close to 1ms. Has this also changed? (I'm on Win7 64 bit). – Ash May 16 '13 at 10:45
  • @AshleyHenderson Yes, it has also changed - I first noticed it with Windows 7 but it probably changed in one of the earlier Server OS versions (post XP). – Matthew Watson May 16 '13 at 11:08
  • _...because Sleep(0) will allow the lower priority threads to run_ You say that a thread calling `Sleep(0)` relinquishes the reminder of its time slice to a thread of lower priority. But what happens in the next line of code right after `Sleep(0)`? The calling thread will have higher priority and regain the CPU. The thread with the lower priority, which should get control will be put on halt by the scheduling to let the original thread continue immediately. Question: Does the thread with lower priority actually gain any CPU at all? – Arno Apr 17 '14 at 06:54
  • And, consequently, what behavior is meant by saying _This behavior changed starting with Windows Server 2003_? `Sleep(0)` doesn't enter any wait state, the argument is `0`. Therefore the issuing thread remains ready to run. It may effectively only relinquish its time slice to threads of equal priority. Threads of higher priority will gain CPU automatically without the need of a `Sleep(0)` at any time when ready to run and threads with lower priority won't get the CPU either because the scheduler will schedule the issuing thread right after the `Sleep(0)` because of its higher priority. – Arno Apr 17 '14 at 09:28
  • @Arno That's exactly the thing that changed. I'm not sure if it really stays ready to run (it might go to WaitSleepJoin if there are other threads to run, I haven't tested this), but the end result is that lower-priority threads *will* take the slice. But just the one slice, usually - not a whole lot of time. I'd just avoid messing with thread priorities, and use higher-level constructs that avoid using `Thread.Sleep` in the first place. – Luaan Aug 19 '15 at 07:56
  • Behaviour of [`SleepEx`](https://msdn.microsoft.com/en-us/library/windows/desktop/ms686307(v=vs.85).aspx) didn't change and [`Thread.Sleep`](https://msdn.microsoft.com/en-us/library/d00bd51t(v=vs.110).aspx) still has an "equal priority" clause. The change in Sleep() behavior looks kind of irrelevant. – default locale Dec 28 '17 at 06:00
  • 2
    @defaultlocale The documentation that you linked for `SleepEx()` states: `Windows XP: A value of zero causes the thread to relinquish the remainder of its time slice to any other thread of equal priority that is ready to run. If there are no other threads of equal priority ready to run, the function returns immediately, and the thread continues execution. This behavior changed starting with Windows Server 2003.` So yes, it did change. – Matthew Watson Mar 03 '18 at 13:45
  • @MatthewWatson Thank you for a clarification! I've managed to miss this part somehow. Sorry for that. – default locale Mar 03 '18 at 13:58