Is Thread.Sleep(1) special?
Asked Answered
F

1

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);
    }
}
Flannel answered 16/5, 2013 at 9:58 Comment(1)
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).Heliometer
C
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.

Crabbing answered 16/5, 2013 at 10:9 Comment(13)
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.Flannel
Sorry I'll add a link to the MSDN docs for Windows API Sleep() ... and done.Crabbing
Great!, that'll teach me for relying on MSDN .NET docs and an article from 2006. Accepted answer.Flannel
@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.Crabbing
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.Flannel
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).Flannel
@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).Crabbing
...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?Intra
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.Intra
@Intra 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.Barros
Behaviour of SleepEx didn't change and Thread.Sleep still has an "equal priority" clause. The change in Sleep() behavior looks kind of irrelevant.Esperanzaespial
@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.Crabbing
@MatthewWatson Thank you for a clarification! I've managed to miss this part somehow. Sorry for that.Esperanzaespial

© 2022 - 2024 — McMap. All rights reserved.