Jump to content
Sign in to follow this  
Tommi Prami

Interesting read about Sleep(0/1) and SwitshToThread

Recommended Posts

Spoiler, SwitshToThread <>  Sleep(0).

https://stackoverflow.com/questions/1383943/switchtothread-vs-sleep1
 

Quote

There are two differences. The first is mentioned in the MSDN docs for SwitchToThread:

The yield of execution is limited to the processor of the calling thread. The operating system will not switch execution to another processor, even if that processor is idle or is running a thread of lower priority.

Sleep(0) will allow threads on other processors to run, as well.


Seems that this is quite tricky, dependig what yuo are doing and what you are trying to achioeve. No silver bullets...

-tee-

Edited by Tommi Prami

Share this post


Link to post

Spent two hours trying to reproduce anything close to that mentioned article https://joeduffyblog.com/2006/08/22/priorityinduced-starvation-why-sleep1-is-better-than-sleep0-and-the-windows-balance-set-manager/

I read that article years ago, many years, yet i couldn't reproduce anything even small hint if that is the case with Sleep(0) vs Sleep(1) vs SwitchToThread, 

So i just wrote two tests, one is mimicking the starvation pointed and presented in the article producer/consumer, and the other just how efficient these three method.

 

Also to be clear, i am not saying the article is misleading or wrong, but it is most likely testing different thing completely (namely the efficiency of ThreadPool in C# in 2006), or the test is happening is single core CPU belongs to past and gone era, know this Sleep(1) by definition can't perform less than OS timer granularity which is by default 1000/64 seconds almost 15-16 ms, and in best case scenario it will be 1 ms, this is guaranteed by the OS, so the article result is irrelevant today.

 

First test 

program ThreadIterationTest;

{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.Classes,
  Winapi.Windows;

{$WARN SYMBOL_PLATFORM OFF}

type
  TSwitchMethod = (smSleep0, smSleep1, smSwitchToThread);

  TCounterThread = class(TThread)
  private
    FSwitchMethod: TSwitchMethod;
    FIterationCount: Int64;
    FDuration: Integer;
    procedure Execute; override;
  public
    constructor Create(SwitchMethod: TSwitchMethod; Duration: Integer; Priority: TThreadPriority);
    property IterationCount: Int64 read FIterationCount;
    property SwitchMethod: TSwitchMethod read FSwitchMethod;
  end;

constructor TCounterThread.Create(SwitchMethod: TSwitchMethod; Duration: Integer; Priority: TThreadPriority);
begin
  inherited Create(True); // Create suspended
  FSwitchMethod := SwitchMethod;
  FDuration := Duration;
  FIterationCount := 0;
  Self.Priority := Priority;
  FreeOnTerminate := True;
end;

procedure TCounterThread.Execute;
var
  StartTick, Duration: Cardinal;
begin
  Duration := FDuration * 1000;

  StartTick := GetTickCount;
  while (GetTickCount - StartTick) < Duration do
  begin
    Inc(FIterationCount);
    case FSwitchMethod of {(*}
      smSleep0: Sleep(0);
      smSleep1: Sleep(1);
      smSwitchToThread: SwitchToThread;   {*)}
    end;
  end;
end;

function ThPriorityToString(ThProirity: TThreadPriority): string;
var
  P: Integer;
begin
  case ThProirity of    {(*}
    tpIdle: Result:= 'Idle';
    tpLowest: Result:= 'Lowest';
    tpLower: Result:= 'Lower';
    tpNormal: Result:= 'Normal';
    tpHigher: Result:= 'Higher';
    tpHighest: Result:= 'Highest';
    tpTimeCritical: Result:= 'TimeCritical';  {*)}
  else
    Result := 'Unknown';
  end;
  Result := Result + '(';
  case ThProirity of      {(*}
    tpIdle: P:= THREAD_PRIORITY_IDLE;
    tpLowest: P:= THREAD_PRIORITY_LOWEST;
    tpLower: P:= THREAD_PRIORITY_BELOW_NORMAL;
    tpNormal: P:= THREAD_PRIORITY_NORMAL;
    tpHigher: P:= THREAD_PRIORITY_ABOVE_NORMAL;
    tpHighest: P:= THREAD_PRIORITY_HIGHEST;
    tpTimeCritical: P:= THREAD_PRIORITY_TIME_CRITICAL;   {*)}
  else
    P := 999;
  end;
  Result := Result + IntToStr(P) + ')';
end;

procedure RunTest(Duration: Integer; Priority1, Priority2, Priority3: TThreadPriority);
var
  Thread1, Thread2, Thread3: TCounterThread;
begin
  Writeln('Starting test with duration: ', Duration, ' seconds');
  Writeln('Thread priorities: Sleep(0)=', ThPriorityToString(Priority1), ', Sleep(1)=', ThPriorityToString(Priority2), ', SwitchToThread=', ThPriorityToString(Priority3));

  Thread1 := TCounterThread.Create(smSleep0, Duration, Priority1);
  Thread2 := TCounterThread.Create(smSleep1, Duration, Priority2);
  Thread3 := TCounterThread.Create(smSwitchToThread, Duration, Priority3);

  Thread1.Start;
  Thread2.Start;
  Thread3.Start;

  WaitForSingleObject(Thread1.Handle, INFINITE);
  WaitForSingleObject(Thread2.Handle, INFINITE);
  WaitForSingleObject(Thread3.Handle, INFINITE);

  Writeln('Results:');
  Writeln('Sleep(0) iterations: ', Thread1.IterationCount);
  Writeln('Sleep(1) iterations: ', Thread2.IterationCount);
  Writeln('SwitchToThread iterations: ', Thread3.IterationCount);
  Writeln;
end;

begin
  try

    Writeln('Test 1: All threads with normal priority');
    RunTest(3, tpNormal, tpNormal, tpNormal);

    Writeln('Test 1.1: All threads with normal priority');
    RunTest(1, tpNormal, tpNormal, tpNormal);

    Writeln('Test 1.5: All threads with normal priority');
    RunTest(5, tpNormal, tpNormal, tpNormal);

    Writeln('Test 2: Different priorities');
    RunTest(5, tpHigher, tpNormal, tpLower);

    Writeln('Test 3: Different priorities');
    RunTest(5, tpLowest, tpHighest, tpNormal);

    Writeln('Test 4: Different priorities');
    RunTest(5, tpLowest, tpLowest, tpLowest);

    Writeln('Done.');
    Readln;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

Its result on my device

Test 1: All threads with normal priority
Starting test with duration: 3 seconds
Thread priorities: Sleep(0)=Normal(0), Sleep(1)=Normal(0), SwitchToThread=Normal(0)
Results:
Sleep(0) iterations: 15700875
Sleep(1) iterations: 175
SwitchToThread iterations: 19869985

Test 1.1: All threads with normal priority
Starting test with duration: 1 seconds
Thread priorities: Sleep(0)=Normal(0), Sleep(1)=Normal(0), SwitchToThread=Normal(0)
Results:
Sleep(0) iterations: 5266693
Sleep(1) iterations: 60
SwitchToThread iterations: 6658333

Test 1.5: All threads with normal priority
Starting test with duration: 5 seconds
Thread priorities: Sleep(0)=Normal(0), Sleep(1)=Normal(0), SwitchToThread=Normal(0)
Results:
Sleep(0) iterations: 26351894
Sleep(1) iterations: 269
SwitchToThread iterations: 33344803

Test 2: Different priorities
Starting test with duration: 5 seconds
Thread priorities: Sleep(0)=Higher(1), Sleep(1)=Normal(0), SwitchToThread=Lower(-1)
Results:
Sleep(0) iterations: 26332342
Sleep(1) iterations: 299
SwitchToThread iterations: 33324362

Test 3: Different priorities
Starting test with duration: 5 seconds
Thread priorities: Sleep(0)=Lowest(-2), Sleep(1)=Highest(2), SwitchToThread=Normal(0)
Results:
Sleep(0) iterations: 26220753
Sleep(1) iterations: 299
SwitchToThread iterations: 33216074

Test 4: Different priorities
Starting test with duration: 5 seconds
Thread priorities: Sleep(0)=Lowest(-2), Sleep(1)=Lowest(-2), SwitchToThread=Lowest(-2)
Results:
Sleep(0) iterations: 26350390
Sleep(1) iterations: 291
SwitchToThread iterations: 33374685

Done.

Sleep(1) is in the expected range of 60-64 per second

 

Now different test to emulate the article example, not using Delphi RTL thread pool and anonymous thread, because i never trust them on my XE8.

program ThreadStarvationTest;

{$APPTYPE CONSOLE}

uses
  System.SysUtils,
  System.Classes,
  Winapi.Windows;

{$WARN SYMBOL_PLATFORM OFF}

type
  TYieldMethod = (ymSleep0, ymSleep1, ymSwitchToThread);

  TStarvationThread = class(TThread)
  private
    FYieldMethod: TYieldMethod;
    FIsProducer: Boolean;
    FDuration: Cardinal;
    procedure Execute; override;
  public
    constructor Create(YieldMethod: TYieldMethod; IsProducer: Boolean; Priority: TThreadPriority);
    property Duration: Cardinal read FDuration;
  end;

var
  x: Integer = 0;

constructor TStarvationThread.Create(YieldMethod: TYieldMethod; IsProducer: Boolean; Priority: TThreadPriority);
begin
  inherited Create(True);
  FYieldMethod := YieldMethod;
  FIsProducer := IsProducer;
  Self.Priority := Priority;
  //FreeOnTerminate := True;    // don't care, irrelevant
  FDuration := 0;
end;

procedure TStarvationThread.Execute;
var
  StartTick: Cardinal;
begin
  // run threads on one core, core 0
  SetThreadAffinityMask(GetCurrentThread, 1);

  if FIsProducer then
  begin
    //Sleep(50);  // Sleep(500); // Sleep(1500);
    x := 1; // Producer sets x
  end
  else
  begin
    StartTick := GetTickCount;
    while x = 0 do
    begin
      case FYieldMethod of
        ymSleep0: Sleep(0);
        ymSleep1: Sleep(1);
        ymSwitchToThread: SwitchToThread;
      end;
    end;
    FDuration := GetTickCount - StartTick;
  end;
end;

function ThPriorityToString(ThPriority: TThreadPriority): string;
var
  P: Integer;
begin
  case ThPriority of   {(*}
    tpIdle: Result := 'Idle';
    tpLowest: Result := 'Lowest';
    tpLower: Result := 'Lower';
    tpNormal: Result := 'Normal';
    tpHigher: Result := 'Higher';
    tpHighest: Result := 'Highest';
    tpTimeCritical: Result := 'TimeCritical';   {*)}
  else
    Result := 'Unknown';
  end;
  Result := Result + '(';
  case ThPriority of    {(*}
    tpIdle: P := THREAD_PRIORITY_IDLE;
    tpLowest: P := THREAD_PRIORITY_LOWEST;
    tpLower: P := THREAD_PRIORITY_BELOW_NORMAL;
    tpNormal: P := THREAD_PRIORITY_NORMAL;
    tpHigher: P := THREAD_PRIORITY_ABOVE_NORMAL;
    tpHighest: P := THREAD_PRIORITY_HIGHEST;
    tpTimeCritical: P := THREAD_PRIORITY_TIME_CRITICAL;  {*)}
  else
    P := 999;
  end;
  Result := Result + IntToStr(P) + ')';
end;

function YieldMethodToStr(YieldMethod:TYieldMethod):string;
begin
  case YieldMethod of    {(*}
    ymSleep0: Result := 'Sleep(0)';
    ymSleep1: Result := 'Sleep(1)';
    ymSwitchToThread: Result := 'SwitchToThread';   {*)}
  end;
end;

procedure RunStarvationTest(YieldMethod: TYieldMethod; ConsumerPriority, ProducerPriority: TThreadPriority);
var
  Consumer, Producer: TStarvationThread;
begin
  Writeln('Starting starvation test with ', YieldMethodToStr(YieldMethod), ', Consumer=', ThPriorityToString(ConsumerPriority), ', Producer=', ThPriorityToString(ProducerPriority));
  x := 0;

  Consumer := TStarvationThread.Create(YieldMethod, False, ConsumerPriority);
  Producer := TStarvationThread.Create(YieldMethod, True, ProducerPriority);

  Consumer.Start;
  Producer.Start;

  Consumer.WaitFor;
  Producer.WaitFor;

  Writeln('Result: ', YieldMethodToStr(YieldMethod), ' time: ', Consumer.Duration, ' ms');
  Writeln;
end;

begin
  try

    // Test Sleep(0) with equal priorities
    RunStarvationTest(ymSleep0, tpNormal, tpNormal);

    // Test Sleep(0) with different priorities
    RunStarvationTest(ymSleep0, tpNormal, tpLower);

    // Test Sleep(0) with different priorities
    RunStarvationTest(ymSleep0, tpLower, tpNormal);

    // Test Sleep(1) with equal priorities
    RunStarvationTest(ymSleep1, tpNormal, tpNormal);

    // Test Sleep(1) with different priorities
    RunStarvationTest(ymSleep1, tpNormal, tpLower);

    // Test Sleep(1) with different priorities
    RunStarvationTest(ymSleep1, tpLower, tpNormal);

    // Test SwitchToThread with equal priorities
    RunStarvationTest(ymSwitchToThread, tpNormal, tpNormal);

    // Test SwitchToThread with different priorities
    RunStarvationTest(ymSwitchToThread, tpNormal, tpLower);

    // Test SwitchToThread with different priorities
    RunStarvationTest(ymSwitchToThread, tpLower, tpNormal);


    Writeln('Done.');
    Readln;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

Its result with and without delaying the producer by uncommenting the Sleep(50).. no starvation observed at all and the result is consistent with the delay introduced being Sleep(50), Sleep(500) or Sleep(1500)

Starting starvation test with Sleep(0), Consumer=Normal(0), Producer=Normal(0)
Result: Sleep(0) time: 0 ms

Starting starvation test with Sleep(0), Consumer=Normal(0), Producer=Lower(-1)
Result: Sleep(0) time: 0 ms

Starting starvation test with Sleep(0), Consumer=Lower(-1), Producer=Normal(0)
Result: Sleep(0) time: 0 ms

Starting starvation test with Sleep(1), Consumer=Normal(0), Producer=Normal(0)
Result: Sleep(1) time: 0 ms

Starting starvation test with Sleep(1), Consumer=Normal(0), Producer=Lower(-1)
Result: Sleep(1) time: 15 ms

Starting starvation test with Sleep(1), Consumer=Lower(-1), Producer=Normal(0)
Result: Sleep(1) time: 0 ms

Starting starvation test with SwitchToThread, Consumer=Normal(0), Producer=Normal(0)
Result: SwitchToThread time: 0 ms

Starting starvation test with SwitchToThread, Consumer=Normal(0), Producer=Lower(-1)
Result: SwitchToThread time: 0 ms

Starting starvation test with SwitchToThread, Consumer=Lower(-1), Producer=Normal(0)
Result: SwitchToThread time: 0 ms

Done.

 

4 hours ago, Tommi Prami said:

No silver bullets...

True, there is no silver bullet, but Sleep(1) is better for completely different reason and it is impossible to be less than 1ms delay, unless with my vivid imagination your PC has no other threads running, like the OS has nothing else to do, so it will be faced with either

1) put the CPU/core to sleep, i mean really sleep signal and reduce the power for the core.

2) ignore the delay continue, even in this case, the article result can't be reproduce because we have two threads not one, hence the producer will execute releasing the consumer.

 

ps: these tests are for specific measurement, and i know they are not very helpful in real life, but they are accurate in the delays and shows how SwitchToThread is faster then Sleep(0), due the reduced context switch with threads from different processes, as for mixing the priorities, well this is way different subject and longer, but even so as tests shows it is irrelevant with Windows 10 and modern CPU, my CPU is Sandy Bridge so around 15 years old.

 

I would love to see result form modern and different CPUs, just for general information, also from different Windows versions, that would be nice, the discussion of how much relevant the test in real life doesn't concern me, as i am trying to replicate that article strange result.

Share this post


Link to post

As I understand what they say, for some ypou would need multiple CPUs. 

You for sure might have such a hardware.

Thanks for extra info.,..

 

Share this post


Link to post
3 minutes ago, Tommi Prami said:

As I understand what they say, for some ypou would need multiple CPUs. 

You for sure might have such a hardware.

Thanks for extra info.,..

 

No sure i do understand that, but lets say on single core the test will yield similar result to the article, meaning Sleep(0) is magnitude slower than Sleep(1) and SwitchToThread, then that OS should not be working at all ! and if worked then it will be slower than computer in 70s and 80s, i mean less than 10Mhz CPU.

Share this post


Link to post
Just now, Kas Ob. said:

No sure i do understand that, but lets say on single core the test will yield similar result to the article, meaning Sleep(0) is magnitude slower than Sleep(1) and SwitchToThread, then that OS should not be working at all ! and if worked then it will be slower than computer in 70s and 80s, i mean less than 10Mhz CPU.

One of the replies says that SwitchToThread will yeld to waiting thread on same CPU (not the core). If computer has multiple CPUs then Sleep(0) would run threads on other CPU also. 

That's how I understood the repply. 

But, thread cheduler might have changed over the years, so these might be very hard to be sure how it behaves... 

 

-Tee-

Share this post


Link to post

I think all is linked to the time of SO post, 15 years ago.

Talking about CPU here is about CORE, when there are multiple CPU other things come in play and the sleep is the last of the issues.

 

Take care now ( @Kas Ob. told this) that the real life is different: no one use affinity with one thread (yes, I do it but for really unique needs), the threads work are balanced by ITD (Intel Thread Director) that acts between OS and hardware, and also Windows may have changed some logics.

 

If you look at your thread (not the one in the example that's "blocked" by Affinity), you'll see that it's "moved" by core during its lifetime. That is, a thread doesn't necessarily always run in the same core. So, if Microsoft were to say that SwitchToThread works in a certain way with modern hardware... well, I wouldn't be so sure, or rather, not in the context we're imagining, given that the load distribution is dynamic.

  • Like 1

Share this post


Link to post
3 minutes ago, pyscripter said:

By the way, TThread.Yield is the cross-platform way to call SwitchToThread.

That is where this journey started...  😉

-Tee-

Share this post


Link to post

Create an account or sign in to comment

You need to be a member in order to leave a comment

Create an account

Sign up for a new account in our community. It's easy!

Register a new account

Sign in

Already have an account? Sign in here.

Sign In Now
Sign in to follow this  

×