Using waiting for Task.Delay in performance killing
Let's say I want to start about N jobs per second, equally distributed.
So I tried this:
public async Task Generate(int numberOfCallsPerSecond)
{
var delay = TimeSpan.FromMiliseconds(1000/numberOfCallsPerSecond); // a call should happen every 1000 / numberOfCallsPerSecond miliseconds
for (int i=0; i < numberOfcallsPerSecond; i++)
{
Task t = Call(); // don't wait for result here
await Task.Delay(delay);
}
}
At first I expected this to start in 1 second, but for numberOfCallsPerSecond = 100
it it takes 16 seconds
on my 12 core CPU. It seems like waiting for Task.Delay adds a lot of overhead (of course, without it in place, call generation happens in 3ms.
I didn't expect waiting to add so much overhead in this scenario. This is normal?
EDIT:
Please forget about calling (). Running this code shows a similar result:
public async Task Generate(int numberOfCallsPerSecond)
{
var delay = TimeSpan.FromMiliseconds(1000/numberOfCallsPerSecond); // a call should happen every 1000 / numberOfCallsPerSecond miliseconds
for (int i=0; i < numberOfcallsPerSecond; i++)
{
await Task.Delay(delay);
}
}
I tried to run it with numberOfCallsPerSecond = 500
and it takes about 10 seconds, I expected it to Generate
take about 1 second and not 10 times as long
source to share
Task.Delay
is easy but not accurate. Since the no-delay loop completes much faster, it sounds like your thread is not running and is using OS sleep to wait for the timer to expire. The timer is checked according to the OS thread scheduling quantization (in the same interrupt handler that performs thread preemption), which is 16ms by default.
You can reduce the quantum with timeBeginPeriod
, but the best (more energy efficient) approach if you need to limit the speed rather than the exact time is to keep track of the elapsed time (the class Stopwatch
is good for this) and the number of calls made and only the delay when the calls made got to past tense. The overall effect is that your thread wakes up ~ 60 times per second and starts multiple work items every time it does. If your processor gets busy with something else, you will trigger additional work items when you take control back - although it's also fairly simple to limit the number of items that can run immediately if you need to.
public async Task Generate(int numberOfCallsPerSecond)
{
var elapsed = Stopwatch.StartNew();
var delay = TimeSpan.FromMiliseconds(1000/numberOfCallsPerSecond); // a call should happen every 1000 / numberOfCallsPerSecond miliseconds
for (int i=0; i < numberOfcallsPerSecond; i++)
{
Call(); // don't wait for result here
int expectedI = elapsed.Elapsed.TotalSeconds * numberOfCallsPerSecond;
if (i > expectedI) await Task.Delay(delay);
}
}
source to share
My psychic debugger says that your method Call
has a significant synchronous part (i.e. the before part await
) that takes time to execute synchronously.
If you want the method to Generate
only "run" these calls Call
and run them at the same time (including the synchronous parts), you must offload them to the thread ThreadPool
using Task.Run
:
var task = Task.Run(() => Call());
await Task.Delay(delay);
Task.Delay
adds almost no overhead. It uses an internal System.Threading.Timer
one that requires very few resources.
source to share