【问题标题】:Task.Delay delays too longTask.Delay 延迟太久
【发布时间】:2018-09-09 01:57:55
【问题描述】:

我创建了一个多任务程序。这个程序有大约 20 个主要任务,每个任务都会调用一些子任务来操作文件 I/O。我希望每个主要任务每 500 毫秒定期重复一次,所以我输入了代码 Task.Delay(500)。

问题是 Task.Delay 有时会延迟超过 500 毫秒。有一种情况会延迟超过 3 秒。 我该如何解决?

原来的程序太大了,我在下面创建了一个示例程序。 (1) 如果 Task.Delay 开启,则会发生过延迟。 (2) 如果 Thead.Sleep 开启,则不会发生过延迟。

ThreadPool.SetMinThreads() 似乎无法解决它。

谢谢。

class Program
{

    const int DELAY_TIME = 500;
    const int TASKS = 100;
    const int WAITS = 100;
    const int WARNING_THRESHOLD = 100;

    static void Main(string[] args)
    {
        //ThreadPool.SetMinThreads(workerThreads: 200, completionPortThreads: 200);

        Console.WriteLine("*** Start...");
        Test();
        Console.WriteLine("*** Done!");
        Console.ReadKey();
    }

    private static void Test()
    {
        List<Task> tasks = new List<Task>();
        for (int taskId = 0; taskId < TASKS; taskId++)
        {
            tasks.Add(DelaysAsync(taskId));
        }
        Task.WaitAll(tasks.ToArray());
    }

    static async Task DelaysAsync(int taskId)
    {
        await Task.Yield();

        Stopwatch sw = new Stopwatch();

        for (int i = 0; i < WAITS; i++)
        {
            sw.Reset();
            sw.Start();
            await Task.Delay(DELAY_TIME).ConfigureAwait(false);   // (1)
            //Thread.Sleep(DELAY_TIME);   // (2)
            sw.Stop();

            Console.Write($"Task({taskId})_iter({i}) Elapsed={sw.ElapsedMilliseconds}");
            if (sw.ElapsedMilliseconds > DELAY_TIME + WARNING_THRESHOLD)
            {
                Console.WriteLine(" *********** Too late!! ************");
            }
            else
            {
                Console.WriteLine();
            }
        }

    }
}

【问题讨论】:

  • 听起来可用的线程数量可能有限。 ThreadPool.GetAvailableThreads 你有什么价值?
  • 感谢您的评论。我试过ThreadPool.GetAvailableThreads 并得到workerThreads=2047, completionPortThreads=1000
  • 我的可用线程较少,机器较旧。可能是调试器,我没有调试就运行了,没有遇到你遇到的情况
  • 我在 Release 和 Debug 上运行它以得到相同的结果。

标签: c# .net asynchronous async-await task-parallel-library


【解决方案1】:

我已经使用 .NET 4.6.1 和 VS 2017 运行了您的测试。在 Xeon E3-1230 v3 CPU 上,它从未打印过“太晚”,Elapsed 值在 498-527 毫秒内。

Thread.Sleep 版本的性能非常相似,每次睡眠 500-528 毫秒,但是总执行时间要长得多,因为运行时拒绝创建 100 个 OS 线程,这太多了,因此并行运行的 DelaysAsync 函数不到 100 个.调试器显示 Thread.Sleep 版本有 27 个工作线程,而 Task.Delay 版本只有 9 个工作线程。

我认为您的 PC 上的其他应用程序创建了太多线程并消耗了太多 CPU。 Windows 尝试均匀地负载平衡线程,因此当整个系统受 CPU 限制时,更多本机线程 = 更多 CPU 时间,因此抖动更少。

如果这是你的情况,并且你想在调度程序中优先考虑你的应用程序,而不是使用 Thread.Sleep 和更多线程,raise the priority 你的进程。

【讨论】:

  • 我将优先级提高为高或实时,但徒劳无功。但是我从你那里得到了一个提示。我的 CPU 是 i7-6500U,所以我在 CPU Xeon E5-2630 v4 的其他 PC 上尝试了它,然后没有出现“为时已晚”。所以,我怀疑它很大程度上取决于 CPU 或线程的数量。我暂时没有时间进一步调查,所以我将在下周进行更多调查并在此处报告结果。
  • @nak326 你的笔记本电脑 CPU 还是不错的。该代码绝对不应该延迟 3 秒。我刚刚在我的笔记本电脑上测试过,它有 i3-6157U,延迟在 499-543 毫秒内。我认为您的特定笔记本电脑有些奇怪。可能与节电、ACPI 驱动程序、有问题的软件或您正在运行的驱动程序有关。确保系统或应用程序事件日志中没有错误/警告消息,确保 CPU 未 100% 加载,如果您运行的是非 Microsoft 防病毒软件,请尝试禁用它……
  • @nak326 您也可以尝试升级 .NET 框架。我已经为 4.6.1 构建了测试,因为这是 VS2017 的默认设置,但实际上我在两台 PC 上都安装了 4.6.2。
  • 我的笔记本电脑没有任何异常。而且我已经在使用 .NET Framework 4.6.1 并尝试了其他一些版本。
【解决方案2】:

看来我可以找到答案了。我改变了之前的示例程序,如下所示。主要区别在于使用 StopWatch 或 DateTime 来测量持续时间。 在秒表版本中,会发生许多延迟。 在 DateTime 版本中,没有或至少很少发生延迟。

我猜原因是 StopWatch 和 Task.Delay 都使用的 Timer 的争用。我得出结论,我不应该同时使用 StopWatch 和 Task.Delay。

谢谢。

class Program
{

    const int DELAY_TIME = 500;
    const int TASKS = 100;
    const int WAITS = 100;
    const int WARNING_THRESHOLD = 500;

    static void Main(string[] args)
    {
        using (Process p = Process.GetCurrentProcess())
        {
            p.PriorityClass = ProcessPriorityClass.RealTime;

            //ThreadPool.SetMinThreads(workerThreads: 200, completionPortThreads: 200);
            int workerThreads;
            int completionPortThreads;
            ThreadPool.GetAvailableThreads(out workerThreads, out completionPortThreads);
            Console.WriteLine($"{workerThreads}, {completionPortThreads}");

            Console.WriteLine("*** Start...");
            Test();
            Console.WriteLine("*** Done!");
            Console.ReadKey();
        }
    }

    private static void Test()
    {
        int totalCount = 0;
        List<Task<int>> tasks = new List<Task<int>>();
        for (int taskId = 0; taskId < TASKS; taskId++)
        {
            //tasks.Add(DelaysWithStopWatchAsync(taskId)); // many delays
            tasks.Add(DelaysWithDateTimeAsync(taskId));  // no delays
        }
        Task.WaitAll(tasks.ToArray());
        foreach (var task in tasks)
        {
            totalCount += task.Result;
        }
        Console.WriteLine($"Total counts of deday = {totalCount}");
    }

    static async Task<int> DelaysWithStopWatchAsync(int taskId)
    {
        await Task.Yield();

        int count = 0;
        Stopwatch sw = new Stopwatch();

        for (int i = 0; i < WAITS; i++)
        {
            sw.Reset();
            sw.Start();
            await Task.Delay(DELAY_TIME).ConfigureAwait(false);   // (1)
            //Thread.Sleep(DELAY_TIME);   // (2)
            sw.Stop();

            Console.Write($"task({taskId})_iter({i}) elapsed={sw.ElapsedMilliseconds}");
            if (sw.ElapsedMilliseconds > DELAY_TIME + WARNING_THRESHOLD)
            {
                Console.WriteLine($" *********** Too late!! ************");
                count++;
            }
            else
            {
                Console.WriteLine();
            }
        }
        return count;

    }

    static async Task<int> DelaysWithDateTimeAsync(int taskId)
    {
        await Task.Yield();

        int count = 0;

        for (int i = 0; i < WAITS; i++)
        {
            DateTime start = DateTime.Now;
            await Task.Delay(DELAY_TIME).ConfigureAwait(false);   // (1)
            //Thread.Sleep(DELAY_TIME);   // (2)
            DateTime end = DateTime.Now;
            int duration = (end - start).Milliseconds;

            Console.Write($"Task({taskId})_iter({i}) Elapsed={duration}");
            if (duration > DELAY_TIME + WARNING_THRESHOLD)
            {
                Console.WriteLine($" *********** Too late!! ************");
                count++;
            }
            else
            {
                Console.WriteLine();
            }
        }
        return count;

    }
}

【讨论】:

  • 您是否正在使用 .NET 内核在 linux 上运行您的程序?我看到了与您的原始问题类似的内容。我怀疑秒表在 linux 上表现不佳,尤其是当有问题的代码能够在多个线程上调度时。
  • 没有。我在 Windows10 上运行它。我怀疑 StopWatch 和 Task.Delay 之间存在一些常见的锁定对象。
  • 您可以尝试使用更大的工作线程数(例如 50)调用 ThreadPool.SetMinThreads()。这似乎解决了我遇到的问题。
猜你喜欢
  • 1970-01-01
  • 2021-02-14
  • 2012-05-31
  • 1970-01-01
  • 2015-02-02
  • 2018-10-11
  • 1970-01-01
  • 1970-01-01
  • 2020-05-26
相关资源
最近更新 更多