2014-11-03 166 views
3

假设我想大致N次启动每秒平均分配任务。使用await Task.Delay中的杀死性能

所以,我想这一点:

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

起初我预计这1秒钟内运行,但对于numberOfCallsPerSecond = 100它在我的12核CPU需要16 seconds。 似乎是等待Task.Delay增加了很多的开销(当然没有它的地方生成的通话发生在3ms。

我没想到在这种情况下,等待会增加很多开销。 ?正常

编辑:

请忘了()的调用运行该代码显示similiar结果:

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

我试图用numberOfCallsPerSecond = 500运行它,它需要大约10秒,我的预期Generate取大约1秒,而不是10倍以上

+0

你可以给Call方法一些代码吗?你确定这个方法在返回任务对象之前不需要时间吗? – Fabske 2014-11-03 14:28:14

+0

您在10秒钟内的500个电话的新结果与调度程序量相当一致。 – 2014-11-03 14:41:47

+0

嗨,大家好,请忘记Call()方法,查看我的EDIT – 2014-11-03 14:42:10

回答

12

Task.Delay重量轻但不准确。由于没有延迟的循环完成得更快,这听起来像你的线程空闲,并使用OS睡眠等待定时器过去。定时器根据操作系统线程调度量(在执行线程预占的同一中断处理程序中)进行检查,默认为16ms。

您可以减少timeBeginPeriod的量程,但是如果您需要速率限制而不是精确定时,则更好(更省电)的方法是跟踪经过的时间(Stopwatch类适用于此)和呼叫数只有在通话时间赶上之后才会延迟。总体效果是,你的线程将每秒唤醒〜60次,每次启动一些工作项目。如果你的CPU变得忙于其他事情,当你得到控制权时,你会开始额外的工作项目 - 尽管如果这是你需要的,那么限制一次启动的项目数量也是非常简单的。

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

嘿,很好的解释它的答案:)。我真的很惊讶的开销。试试你的解决方案。 +1 – 2014-11-03 14:51:59

+2

'await Task.Delay()'不会导致线程在给定的时间内休眠。相反,它使用一个计时器并在计时器过去时安排延续执行。所以重要的是计时器的解析度,而不是睡眠的解析度。虽然默认情况下都是16毫秒,这就是观察到的行为与您所描述的行为相同的原因。 – svick 2014-11-05 12:58:17

+0

@svick:我没有说'Task.Delay()'睡眠线程。如果没有任务准备就绪,则任务分派循环。 – 2014-11-05 13:16:48

1

我的心理调试说,你Call方法有哪些需要时间同步执行显著同步部分(即一个await之前的部分)。

如果你想Generate方法仅适用于“火”起来,这些Call电话,并让它们同时运行(包括同步部分),你需要使用Task.Run他们卸载到一个ThreadPool线程:

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

Task.Delay几乎没有增加开销。它在内部使用System.Threading.Timer,只需要很少的资源。

+0

嘿,谢谢你的回答,请你检查一下我的编辑? – 2014-11-03 14:41:42

+0

问题的第一次修改提到“没有'Task.Delay'到位代码在3ms中完成”似乎排除了这个解释 – 2014-11-03 14:49:32

0

如果您使用Task.Delay()的时间跨度,它会杀死CPU。使用一个整数,它不会。真实的故事。不知道为什么。

+0

你可以发布一些代码/基准吗?将是一个很好的补充。 – FRoZeN 2017-01-31 10:18:53

+0

当我找到时间时,我会尽量记得把它们放在一起。这是我去年在写日志Tailer时发现的东西。 – Sinaesthetic 2017-02-01 16:53:05