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

7
我写了一个使用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.SetMinThreads和ThreadPool.SetMaxThreads进行调整,但它甚至没有改变上述繁忙线程的数量。

1
你为什么需要了解每一个延迟呢?难道没有其他指标可以代替吗? - svick
@svick 我需要监控某些特定时间安排的事件,因此时间是我唯一的参考点。 - humanista
我也正在面对完全相同的问题。 - Suchith
3个回答

3
当你执行Task.Run时,它使用线程池线程来执行这些任务。当你有长时间运行的任务时,会导致线程池资源被占用而出现饥饿。
两个建议:
1. 运行长时间运行的任务时,请确保使用Task.Factory.Startnew和TaskCreationOptions.LongRunning,这将触发新线程的创建。在这里你必须小心,因为过多地创建新线程会导致上下文切换过多,从而使你的应用程序变慢。 2. 在需要进行IO绑定工作时,请使用支持TAP的api,如HttpClient和Stream,并使用真正的异步操作,这不会导致新线程执行阻塞式工作。

1
我知道线程池饥饿的问题。令我惊讶的是,即使我在一个不受线程池控制的单独线程中运行日志作业(我只是使用System.Threading.Thread),它仍然会发生。此外,“ThreadPool.GetAvailableThreads”显示只有1个线程正在忙碌。至于您的其他建议,任务并不是长时间运行的,并且我尽可能地使用真正的异步。 - humanista
你正在运行代码的机器有多少个核心? - Yuval Itzchakov
四个核心。Process.GetCurrentProcess().Threads.Count 通常返回约30个,更精确地说,ThreadPool.GetAvailableThreads 返回0个繁忙的工作线程和1-2个繁忙的完成端口线程,这有点奇怪。 - humanista
所以你有大约35个线程在4个核心上同时运行?为什么这么多?这确实是有道理的,因为由于大量的上下文切换,您会觉得任务开始“晚”。 - Yuval Itzchakov
这是一个Windows Forms应用程序,尽管它只有一个带菜单的托盘图标。它启动时有11个线程,但当它每分钟发送数千个请求时,它很快就会增加到30个。我实际上认为这并不算太多。 - humanista
1
我建议您使用性能基准测试工具,对您的应用程序进行一些压力测试,这样您就可以真正挖掘并查看发生了什么。如果您使用Visual Studio 2013,您可以使用内置的性能诊断工具,效果非常好。 - Yuval Itzchakov

2

在异步/等待中存在开销,同时任务本身以较低优先级执行。如果您需要可靠地按准确的间隔发生某些事情,则不应使用异步/等待/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);
}

根据需要调整Sleep(..)和interval值。


尽管运行 Thread.Sleep 会阻塞整个线程,从而什么都不做,但 Task.Delay 更具可扩展性。 - nikeee
1
@nikeee 但是这种可扩展性也有代价,而这基本上就是这个问题所涉及的。 - svick
我不是很喜欢这个解决方案,因为我真的想使用async/await/TPL。它更容易编码,许多.NET Framework对象也支持它。但如果没有其他更好的解决方案出现,我会将其标记为答案。谢谢。 - humanista
-1 相对于使用 TPL、async 来说优势很小。建议改用 await Task.Wait(100).ConfigureAwait(false) - Aron
看起来开销很大。我只是在一个循环中执行Task.Delay(1);(仅此而已),循环迭代超过1000次,却花费了14秒的时间,而不是差不多一秒钟。 - Ben Philipp

2
我认为您正在经历Joe Duffy在他的"CLR线程池注入、卡顿问题"博客文章中描述的情况:

我们的线程池目前做的一个傻事与它如何创建新线程有关。也就是说,一旦您超过“最小”线程数(默认情况下为机器上的CPU数量),它会严重限制创建新线程。一旦达到或超过这个数字,我们在500毫秒内最多只能创建一个新线程。

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

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

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

我稍微尝试了一下,但无论使用哪些数字,ThreadPool.GetAvailableThreads 都返回 0 个繁忙的工作线程和 1-2 个繁忙的完成端口线程。 - humanista
@humanista,Process.GetCurrentProcess().Threads.Count 怎么样?它显示的数字是否与使用 SetMinThreads 请求的数字接近现实? - noseratio - open to work
它返回约30个线程。我刚刚更新了原始帖子,请阅读其中的详细信息。 - humanista
1
@humanista,还可以尝试使用await Task.Delay(5000).ConfigureAwait(false)。在所有不需要UI线程上下文的地方都使用ConfigureAwait(false)。这样做会改变什么吗?另外,你能提供一段代码来重现这个问题吗? - noseratio - open to work

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接