我有一个我不等待的任务,因为我希望它在后台继续自己的逻辑。部分逻辑是延迟60秒并重新检查以确定是否要完成一些微小的工作。缩写代码如下所示:
public Dictionary<string, Task> taskQueue = new Dictionary<string, Task>();
// Entry point
public void DoMainWork(string workId, XmlDocument workInstructions){
// A work task (i.e. "workInstructions") is actually a plugin which might use its own tasks internally or any other logic it sees fit.
var workTask = Task.Factory.StartNew(() => {
// Main work code that interprets workInstructions
// .........
// .........
// etc.
}, TaskCreationOptions.LongRunning);
// Add the work task to the queue of currently running tasks
taskQueue.Add(workId, workTask);
// Delay a period of time and then see if we need to extend our timeout for doing main work code
this.QueueCheckinOnWorkTask(workId); // Note the non-awaited task
}
private async Task QueueCheckinOnWorkTask(string workId){
DateTime startTime = DateTime.Now;
// Delay 60 seconds
await Task.Delay(60 * 1000).ConfigureAwait(false);
// Find out how long Task.Delay delayed for.
TimeSpan duration = DateTime.Now - startTime; // THIS SOMETIMES DENOTES TIMES MUCH LARGER THAN EXPECTED, I.E. 80+ SECONDS VS. 60
if(!taskQueue.ContainsKey(workId)){
// Do something based on work being complete
}else{
// Work is not complete, inform outside source we're still working
QueueCheckinOnWorkTask(workId); // Note the non-awaited task
}
}
请记住,这是示例代码,仅显示我的实际程序正在发生的极小版本。
我的问题是Task.Delay()延迟时间超过指定的时间。在某个合理的时间范围内,有些事情阻止了这一点。
不幸的是,我无法在我的开发机器上复制该问题,而且每隔几天它就会在服务器上发生。最后,它似乎与我们一次运行的工作任务数量有关。
什么会导致延迟超过预期?另外,如何调试这种情况呢?
这是我的另一个问题的后续问题,但没有得到答案:await Task.Delay() delaying for longer that expected
答案 0 :(得分:4)
大多数情况下,这是因为线程池饱和而发生的。您可以通过这个简单的控制台应用程序清楚地看到它的效果(我以与您正在做的相同的方式测量时间,在这种情况下,如果我们使用秒表,则无关紧要):
public class Program {
public static void Main() {
for (int j = 0; j < 10; j++)
for (int i = 1; i < 10; i++) {
TestDelay(i * 1000);
}
Console.ReadKey();
}
static async Task TestDelay(int expected) {
var startTime = DateTime.Now;
await Task.Delay(expected).ConfigureAwait(false);
var actual = (int) (DateTime.Now - startTime).TotalMilliseconds;
ThreadPool.GetAvailableThreads(out int aw, out _);
ThreadPool.GetMaxThreads(out int mw, out _);
Console.WriteLine("Thread: {3}, Total threads in pool: {4}, Expected: {0}, Actual: {1}, Diff: {2}", expected, actual, actual - expected, Thread.CurrentThread.ManagedThreadId, mw - aw);
Thread.Sleep(5000);
}
}
此程序启动100个等待Task.Delay
1-10秒的任务,然后使用Thread.Sleep
5秒钟来模拟继续运行的线程(这是线程池线程)的工作。它还将输出线程池中的线程总数,因此您将看到它随时间的增长情况。
如果运行它,几乎在所有情况下都会看到(前8个除外) - 延迟后的实际时间比预期长很多,在某些情况下延长5倍(延迟3秒但经过15秒)。
那不是因为Task.Delay
是如此不精确。原因是应该在线程池线程上执行await
后继续。请求时,线程池不会始终为您提供线程。它可以考虑不是创建新线程 - 而是等待其中一个当前忙线程完成其工作。它将等待一段时间,如果没有线程变为空闲 - 它仍将创建一个新线程。如果您一次请求10个线程池线程并且没有空闲,则它将等待Xms并创建新线程。现在你有9个队列请求。现在它将再次等待Xms并创建另一个。现在你有8个队列,依此类推。等待线程池线程变为空闲是导致此控制台应用程序延迟增加的原因(并且很可能是在您的实际程序中) - 我们使线程池线程忙于长Thread.Sleep
,并且线程池已经饱和。
线程池使用的一些启发式参数可供您控制。最有影响力的是&#34; minumum&#34;池中的线程数。线程池应始终无延迟地创建新线程,直到池中的线程总数达到可配置的最小值#34;。之后,如果您请求一个线程,它可能仍然会创建一个新线程,或者等待现有线程变为空闲。
因此,消除此延迟的最直接方法是增加池中的最小线程数。例如,如果您这样做:
ThreadPool.GetMinThreads(out int wt, out int ct);
ThreadPool.SetMinThreads(100, ct); // increase min worker threads to 100
上述示例中的所有任务都将在预期时间内完成,不会有任何额外延迟。
这通常不是推荐的方法来解决这个问题。避免在线程池线程上执行长时间运行繁重的操作会更好,因为线程池是一个全局资源,这样做会影响整个应用程序。例如,如果我们在上面的示例中删除Thread.Sleep(5000)
- 所有任务都会延迟预期的时间,因为现在所有使线程池线程忙的是Console.WriteLine
语句,它立即完成,线程可用于其他工作。
总而言之:确定在线程池线程上执行繁重工作的位置,并避免这样做(在单独的非线程池线程上执行繁重的工作)。或者,您可以考虑将池中的最小线程数增加到合理的数量。