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

我写了一个multithreading应用程序,它广泛使用async / await。 它应该在预定的时间下载一些东西。 为此,它使用’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.GetMaxThreads减去ThreadPool.GetAvailableThreads结果,我得到0个繁忙的工作线程和1-2个繁忙的完成端口线程。 Process.GetCurrentProcess().Threads.Count通常返回大约30.它是一个Windows窗体应用程序,虽然它只有一个带菜单的托盘图标,但它以11个线程开始。 当它每分钟发送数千个请求时,它很快就会达到30个。

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

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

2意见建议:

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

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

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

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

我想你正在经历Joe Duffy在他的“CLR线程池注入,口吃问题”博客文章中所描述的情况:

我们的线程池当前所做的一件愚蠢的事情与它如何创建新线程有关。 也就是说,一旦超过“最小”线程数,它会严重限制新线程的创建,默认情况下,这是线程上CPU的数量。 一旦达到或超过此数字,我们将自己限制为每500毫秒最多一个新线程。

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

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

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