等待Task.Delay花费的时间比预期的要长

时间:2014-04-19 15:10:29

标签: c# .net asynchronous task-parallel-library async-await

我编写了一个多线程应用程序,它广泛使用async / await。它应该在预定的时间下载一些东西。为此,它使用等待Task.Delay'。有时它每分钟发送数千个请求。

它按预期工作,但有时我的程序需要记录大的东西。如果是这样,它会序列化许多对象并将它们保存到文件中。在那段时间里,我注意到我的计划任务执行得太晚了。我已将所有日志记录放到具有最低优先级的单独线程中,并且问题不再发生,但它仍然会发生。事情是,我想知道它何时发生,以便知道我必须使用类似的东西:

var delayTestDate = DateTime.Now;
await Task.Delay(5000);
if((DateTime.Now - delayTestDate).TotalMilliseconds > 6000/*delays up to 1 second are tolerated*/) Console.WriteLine("The task has been delayed!");

此外,我发现我也使用的“Task.Run'”也会导致延迟。为了监控它,我必须使用更丑陋的代码:

var delayTestDate = DateTime.Now;
await Task.Run(() =>
{
  if((DateTime.Now - delayTestDate).TotalMilliseconds > 1000/*delays up to 1 second are tolerated*/) Console.WriteLine("The task has been delayed!");
  //do some stuff
  delayTestDate = DateTime.Now;
});
if((DateTime.Now - delayTestDate).TotalMilliseconds > 1000/*delays up to 1 second are tolerated*/) Console.WriteLine("The task has been delayed!");

我必须在每次await和Task.Run之前和之后以及每个异步函数内部使用它,这是丑陋和不方便的。我无法将它放入一个单独的函数中,因为它必须是异步的,我无论如何都要等待它。有没有人想过更优雅的解决方案?

编辑:

我在评论中提供的一些信息:

正如@YuvalItzchakov所注意到的,问题可能是线程池饥饿造成的。这就是为什么我使用System.Threading.Thread来处理线程池之外的日志记录的原因,但正如我所说,问题仍然有时会发生。

我有一个带有四个内核的处理器,通过从ThreadPool.GetAvailableThreads中减去ThreadPool.GetMaxThreads的结果,我得到0个繁忙的工作线程和1-2个繁忙的完成端口线程。 Process.GetCurrentProcess().Threads.Count通常返回大约30个。它是一个Windows窗体应用程序,虽然它只有一个带菜单的托盘图标,但它以11个线程开始。当它每分钟发送数千个请求时,它很快就会达到30个。

正如@Noseratio建议的那样,我尝试使用ThreadPool.SetMinThreadsThreadPool.SetMaxThreads,但它甚至没有改变上面提到的忙线程数。

3 个答案:

答案 0 :(得分:3)

执行Task.Run时,它使用线程池线程来执行这些任务。当你有长时间运行的任务时,你正在导致线程池的饥饿,因为它的资源当前被长时间运行的任务所占用。

2建议:

  1. 运行长时间运行的任务时,请确保将Task.Factory.Startnew与TaskCreationOptions.LongRunning一起使用,这将触发新的线程创建。你也必须谨慎,因为旋转过多的新线程会导致过多的上下文切换,导致你的应用程序变慢

  2. 使用true async,你必须做IO绑定工作,使用支持TAP的api,如HttpClient和Stream,这不会导致新线程执行阻塞工作。

答案 1 :(得分:2)

async / await中存在开销,以及以较低优先级执行的任务本身。如果您需要以准确的时间间隔可靠地发生某些事情,则async / await / TPL不是要使用的接口。

尝试创建一个循环的独立后台线程,直到它被安排工作。这样,您可以直接控制优先级和时间,而无需通过TPL / async。

Thread backgroundThread = new Thread(BackgroundWork);
DateTime nextInterval = DateTime.Now;

public void BackgroundWork()
{
    if(DateTime.Now > nextInterval){
        DoWork();
        nextInterval = nextInterval.Add(new TimeSpan(0,0,0,10)); // 10 seconds
    }
    Thread.Sleep(100);
}

根据需要调整睡眠(..)和间隔值。

答案 2 :(得分:1)

我认为您正在体验Joe Duffy在"CLR thread pool injection, stuttering problems"博文中所描述的情况:

  

我们的线程池目前所做的一件愚蠢的事情与它如何有关   创建新线程。也就是说,它严重限制了新的创造   线程一旦超过“最小”线程数,通过   default,是计算机上的CPU数。我们限制自己   一旦达到或超过这个数字,每500ms最多一个新线程。

一种解决方案可能是在使用TPL之前显式增加线程池线程的最小数量,例如:

ThreadPool.SetMaxThreads(workerThreads: 200, completionPortThreads: 200);
ThreadPool.SetMinThreads(workerThreads: 100, completionPortThreads: 100);

尝试使用这些数字,看看问题是否消失。