【问题标题】:Effects of non-awaited Task非等待任务的影响
【发布时间】:2018-02-21 05:55:02
【问题描述】:

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

【问题讨论】:

    标签: c# .net async-await


    【解决方案1】:

    大多数情况下,由于线程池饱和而发生这种情况。你可以通过这个简单的控制台应用程序清楚地看到它的效果(我测量时间的方式和你一样,在这种情况下我们是否使用秒表并不重要):

    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 秒来模拟在运行 continuation 的线程上的工作(这是线程池线程)。它还将输出线程池中的线程总数,因此您将看到它如何随着时间的推移而增加。

    如果您运行它,您会发现几乎在所有情况下(前 8 次除外) - 延迟后的实际时间比预期的长得多,在某些情况下长 5 倍(您延迟了 3 秒,但已经过去了 15 秒)。

    这不是因为Task.Delay 如此不精确。原因是await 应该在线程池线程上执行之后继续。当您请求时,线程池并不总是给您一个线程。它可以考虑与其创建新线程,不如等待当前繁忙的线程之一完成其工作。它会等待一段时间,如果没有线程空闲 - 它仍然会创建一个新线程。如果你一次请求 10 个线程池线程并且没有一个是空闲的,它将等待 Xms 并创建一个新的。现在队列中有 9 个请求。现在它将再次等待 Xms 并创建另一个。现在你有 8 个在队列中,依此类推。等待线程池线程空闲是导致此控制台应用程序延迟增加的原因(并且很可能在您的实际程序中) - 我们让线程池线程忙于长时间Thread.Sleep,并且线程池已饱和。

    线程池使用的一些启发式参数可供您控制。最有影响力的是池中的“最小”线程数。线程池应始终无延迟地创建新线程,直到池中的线程总数达到可配置的“最小值”。之后,如果您请求一个线程,它可能仍会创建新线程或等待现有线程空闲。

    因此,消除这种延迟的最直接方法是增加池中的最小线程数。例如,如果您这样做:

    ThreadPool.GetMinThreads(out int wt, out int ct);
    ThreadPool.SetMinThreads(100, ct); // increase min worker threads to 100
    

    以上示例中的所有任务都将在预期时间完成,不会有额外的延迟。

    虽然这通常不是解决此问题的推荐方法。最好避免在线程池线程上执行长时间运行的繁重操作,因为线程池是全局资源,这样做会影响整个应用程序。例如,如果我们在上面的示例中删除Thread.Sleep(5000) - 所有任务都会延迟预期的时间量,因为现在让线程池线程忙碌的是Console.WriteLine 语句,它立即完成,使该线程可用于其他工作。

    总结一下:确定您在线程池线程上执行繁重工作的地方并避免这样做(改为在单独的非线程池线程上执行繁重的工作)。或者,您可以考虑将池中的最小线程数增加到合理的数量。

    【讨论】:

    • 谢谢@Evk,这让我找到了我一直在努力寻找的东西。我应该注意,我所有的Task.Factory.StartNew() 调用都标记为LongRunning(我已经更新了我的示例来说明这一点),因为它们要么在其中使用 .Wait() 进行阻塞,要么因为插件正在执行的实际工作可能是繁重的(或者可能不是,我不知道),我试图将密集的、长时间运行的“工作”与阻塞我的线程隔离开来。任何有关如何解决此问题的建议将不胜感激。
    • 为什么不使用'workTask.ContinueWith(...)'?如果可以立即通知您,那么每分钟轮询一次又有什么意义?
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2014-09-06
    相关资源
    最近更新 更多