未等待任务的影响

时间:2018-02-20 03:25:23

标签: c# .net async-await

我有一个我不等待的任务,因为我希望它在后台继续自己的逻辑。部分逻辑是延迟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

1 个答案:

答案 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语句,它立即完成,线程可用于其他工作。

总而言之:确定在线程池线程上执行繁重工作的位置,并避免这样做(在单独的非线程池线程上执行繁重的工作)。或者,您可以考虑将池中的最小线程数增加到合理的数量。