Pause a thread for less than one millisecond

2020-08-09 11:20发布

问题:

In a messaging client test application, the producer thread needs to be throttled to avoid flooding the server.

As the transfer rates are around 25,000 messages per second (40 microseconds per message), the delay caused by Sleep(1) would far too long.

How to make thread sleep less than a millisecond on Windows contains some information related to the Windows API. Is there a code snippet, class or library for Delphi?


Following Bens answer I found that Sleep with different values below 15 also gives different transfer rates (Windows Vista):

Sleep(1) after every 20 messages:

00:02 tx/rx 25740/3705 12831/1846 msgs/sec (77/541 microsecs/msg)
00:04 tx/rx 53101/7405 13255/1848 msgs/sec (75/541 microsecs/msg)
00:06 tx/rx 79640/11288 13260/1879 msgs/sec (75/532 microsecs/msg)
00:08 tx/rx 104520/14562 13055/1818 msgs/sec (76/550 microsecs/msg)
00:10 tx/rx 130760/18829 13066/1881 msgs/sec (76/531 microsecs/msg)

Sleep(5) after every 20 messages:

00:02 tx/rx 7640/3622 3812/1807 msgs/sec (262/553 microsecs/msg)
00:04 tx/rx 14660/10794 3661/2695 msgs/sec (273/371 microsecs/msg)
00:06 tx/rx 21480/18171 3577/3026 msgs/sec (279/330 microsecs/msg)
00:08 tx/rx 28140/25642 3515/3203 msgs/sec (284/312 microsecs/msg)
00:10 tx/rx 34980/32692 3496/3267 msgs/sec (286/306 microsecs/msg)

This was unexpected after reading the comment about the lower limit for busy waiting

And the values without throttling

00:02 tx/rx 44065/494 21988/246 msgs/sec (45/4065 microsecs/msg)
00:04 tx/rx 90493/756 22595/188 msgs/sec (44/5319 microsecs/msg)
00:06 tx/rx 142982/907 23810/151 msgs/sec (41/6622 microsecs/msg)
00:08 tx/rx 192562/1144 24055/142 msgs/sec (41/7042 microsecs/msg)
00:10 tx/rx 237294/1395 23717/139 msgs/sec (42/7194 microsecs/msg)

回答1:

Send 20 messages and then sleep for 1 millisecond?

You can't sleep for less than the scheduler quantum, unless you have a hardware interrupt other than the system timer. Read the answers in the question you linked, they explain why the suggested approach doesn't actually work.

Even with this approach you might sleep longer than 1ms, or since the messages aren't sent instantaneously, the whole operation will surely take longer than 1ms, reducing the overall rate.

So, inspect a precision clock source every time you wake, and calculate how many messages to send based on elapsed time, don't use a constant 20.



回答2:

1) Get the current time.

2) Calculate how many messages you need to send based on how many you've sent and how much time has past since then.

3) Send that many messages.

4) Sleep for the smallest amount you can.

5) Go to step 1.



回答3:

As others have said, you cannot sleep for short amounts of time (and even Sleep(1) is not reliable - it could easily sleep for much more than 1 ms).

Your best bet is to calculate the time for the next message and then execute a busy wait - loop and check time - until the desired time occurs. Below is a full solution together with a small test framework.

program SendEquidistantMessages;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  Windows, SysUtils;

procedure SendMessage(msgNum: integer);
begin
  // send the message here
end;

procedure WaitUntil(nextMsgTime: int64);
var
  currTime: int64;
begin
  repeat
    QueryPerformanceCounter(currTime);
    if currTime >= nextMsgTime then
      break; //repeat
    asm pause; end;
  until false;
end;

procedure SendMessages(numMsg, msgPerSec: integer);
var
  iMsg       : integer;
  nextMsgTime: int64;
  perfFreq   : int64;
  prevMsg    : int64;
  startTime  : int64;

begin
  Assert(QueryPerformanceFrequency(perfFreq));
  Assert(QueryPerformanceCounter(startTime));
  for iMsg := 1 to numMsg do begin
    WaitUntil(Round(startTime + iMsg/msgPerSec * perfFreq));
    SendMessage(iMsg);
  end;
end;

var
  time: cardinal;

begin
  try
    time := GetTickCount;
    SendMessages(20000, 5000);
    time := GetTickCount-time;
    Writeln('20.000 messages sent in ', time/1000:4:1, ' sec; ',
      'required rate = 5000 msg/sec, real rate = ', 20000/(time/1000):6:1, ' msg/sec');
    Readln;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.


回答4:

Instead of sleeping, why not use TTheard.Yield to let another thread/process have a bash at the processor?



回答5:

There is a thread Sleep Less Than One Millisecond dealing with similar problems. I've given some details on how to let a thread sleep for any time. This of course also includes sleeps in the microsecond range. Topics: Timed events created by service threads, Wait functions

More details can also be found at the Windows Timestamp Project



回答6:

a workaround is to get the current time with nano seconds and addition it with the time you want to sleep (wait) and loop until the current time (with nano seconds) become greater than the time you calculated, there is a method that illustrate this:

public void wait(long nano){
    long t= System.nanoTime();
    t+= nano;
    while(System.nanoTime()<t);
}

Note that: 1 second= 1000 milliseconds= 1000000 microseconds= 1000000000 nanoseconds

so

1 millisecond= 1000000 nanoseconds