简体   繁体   中英

using await Task.Delay in a for kills performance

Let's say I want to start roughly N tasks per second distributed equally.

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 run in 1 second but for numberOfCallsPerSecond = 100 it takes 16 seconds on my 12 core CPU. It seems the await Task.Delay adds a lot of overhead (of course without it in place generation of the calls happens in 3ms.

I didn't expect that await would add so much overhead in this scenario. Is this normal?

EDIT:

Please forget about the Call(). Running this code shows similiar 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 around 10 seconds, I expected Generate to take roughly 1 second, not 10 times more

Task.Delay is lightweight but not accurate. Since the loop without delay completes much faster, it sounds like your thread is going idle and using an OS sleep to wait for the timer to elapse. The timer is checked according to the OS thread scheduling quantum (in the same interrupt handler which performs thread pre-emption), which is 16ms by default.

You can reduce the quantum with timeBeginPeriod , but a better (more power efficient) approach if you need rate limiting rather than exact timing is to keep track of elapsed time (the Stopwatch class is good for this) and number of calls made, and only delay when calls made have caught up to elapsed time. The overall effect is that your thread will wake up ~60 times per second, and start a few work items each time it does. If your CPU becomes busy with something else, you'll start extra work items when you get control back -- although it's also pretty straightforward to cap the number of items started at once, if that's what you need.

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);
    }
}

My psychic debugger says your Call method has a significant synchronous part (ie the part before an await ) which takes time to execute synchronously.

If you want the Generate method only to "fire up" these Call calls and have them run concurrently (including the synchronous parts) you need to offload them to a ThreadPool thread using Task.Run :

var task = Task.Run(() => Call());
await Task.Delay(delay);

Task.Delay adds almost no overhead. It uses a System.Threading.Timer internally that requires very little resources.

If you use a timespan with Task.Delay(), it'll kill the CPU. Use an integer and it wont. True story. no idea why.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM