【问题标题】:Very poor performance of async task run on threadpool in .Net native在.Net native 中的线程池上运行的异步任务的性能非常差
【发布时间】:2016-04-12 11:50:19
【问题描述】:

我观察到托管代码与 .Net 本机代码的奇怪差异。我有一项繁重的工作被重定向到线程池。在托管代码中运行应用程序时,一切都很顺利,但只要我打开本机编译 - 任务运行速度会慢几倍,而且速度很慢,以至于它挂起了 UI 线程(我猜 CPU 已经超载了)。

这里有两张来自调试输出的截图,左边一张来自托管代码,右边一张来自本机编译。如您所见,两种情况下 UI 任务消耗的时间几乎相同,直到线程池作业启动时 - 然后在托管版本中,UI 运行时间会增加(实际上 UI 被阻塞,您无法采取任何操作)。线程池工作的时间不言自明。

重现问题的示例代码:

private int max = 2000;
private async void UIJob_Click(object sender, RoutedEventArgs e)
{
    IProgress<int> progress = new Progress<int>((p) => { MyProgressBar.Value = (double)p / max; });
    await Task.Run(async () => { await SomeUIJob(progress); });
}

private async Task SomeUIJob(IProgress<int> progress)
{
    Stopwatch watch = new Stopwatch();
    watch.Start();
    for (int i = 0; i < max; i++)
    {
        if (i % 100 == 0) { Debug.WriteLine($"     UI time elapsed => {watch.ElapsedMilliseconds}"); watch.Restart(); }
        await Task.Delay(1);
        progress.Report(i);
    }
}

private async void ThreadpoolJob_Click(object sender, RoutedEventArgs e)
{
    Debug.WriteLine("Firing on Threadpool");
    await Task.Run(() =>
   {
       double a = 0.314;
       Stopwatch watch = new Stopwatch();
       watch.Start();
       for (int i = 0; i < 50000000; i++)
       {
           a = Math.Sqrt(a) + Math.Sqrt(a + 1) + i;
           if (i % 10000000 == 0) { Debug.WriteLine($"Threadpool -> a value = {a} got in {watch.ElapsedMilliseconds} ms"); watch.Restart(); };
       }
   });
    Debug.WriteLine("Finished with Threadpool");
}

如果您需要完整的样本 - 那么您可以download it here

正如我测试的那样,无论是在调试版本还是发布版本中,优化/非优化代码都会出现差异。

有人知道什么会导致问题吗?

【问题讨论】:

  • 可能需要看看发出的 IL 和机器码。
  • 我在 .NET Native Compiler and Runtime 团队工作。我们通常使用 PerfView 进行此类调查。如果您可以收集一些 etl 跟踪(一个带有 .net native 和一个不带)并将它们发送到我们的方式 (dotnetnative@microsoft.com),我们会找人看一看。
  • 可能是线程池不足。你玩过ThreadPool.SetMinThreads/SetMaxThreads吗?
  • @Noseratio 好像在 UWP 中没有控制线程数的选项。
  • @MattWhilden 我已经向您提到的地址发送了一封电子邮件。我观察到这主要是针对部署在 ARM 设备上的应用程序 - 是否可以为此类进程运行 perview?

标签: c# async-await win-universal-app uwp .net-native


【解决方案1】:

这个问题是因为“线程池”数学循环导致 GC 饥饿。本质上,GC 已经决定它需要运行(由于想要进行一些互操作分配)并且它试图停止所有线程来进行收集/压缩。不幸的是,我们还没有添加 .NET Native 劫持热循环的功能,就像您在下面看到的那样。这在 Migrating Your Windows Store App to .NET Native 页面上被简要提及为:

在任何线程上不进行调用的无限循环(例如,while(true);)可能会使应用程序停止。同样,长时间等待或无限等待可能会导致应用停止。

解决此问题的一种方法是将调用站点添加到您的循环中(GC 很乐意在尝试调用另一个方法时中断您的线程!)。

    for (long i = 0; i < 5000000000; i++)
           {
               MaybeGCMeHere(); // new callsite
               a = Math.Sqrt(a) + Math.Sqrt(a + 1) + i;
               if (i % 1000000000 == 0) { Debug.WriteLine($"Threadpool -> a value = {a} got in {watch.ElapsedMilliseconds} ms"); watch.Restart(); };
    }

...

    [MethodImpl(MethodImplOptions.NoInlining)] // need this so the callsite isn’t optimized away
    private void MaybeGCMeHere()
    {
    }

缺点是你会有这个“丑陋”的hack,你可能会因为添加的指令而受苦。我已经让这里的一些人知道,这个我们认为“极其罕见”的东西实际上是被客户击中的,我们会看看能做些什么。

感谢您的报告!

更新:我们已经围绕这种情况进行了一些重大改进,并将能够劫持大多数长时间运行的线程以进行 GC。这些修复程序可能会在 4 月发布的 UWP 工具更新 2 集中提供? (我不控制运输时间表:-))

更新更新:新工具现已作为 UWP 工具 1.3.1 的一部分提供。我们不希望有一个完美的解决方案来积极对抗被 GC 劫持的线程,但我希望使用最新的工具,这种情况会好得多。让我们知道!

【讨论】:

  • 感谢整个团队的照顾。你的一位同事说,这将在下一次 VS 更新中得到纠正——太好了。我同意在桌面上重现这个问题对我来说更难,但在 ARM 上我认为这是一个真实的场景——事实上我已经在我的应用程序中观察到了这一点。我有一种方法可以处理照片并对像素进行一些数学运算,因为它消耗 cpu,它被重定向到线程池,这就是我发现问题的地方。再次感谢您。
  • 我还对您的答案进行了少量编辑,并将 MSDN 链接设为粗体 - 这有可能帮助某人节省一些时间。
  • 编辑很棒!我的 SO 标记不是很好,所以我真的很感激!听起来我们会在更新 2 中对这类事情进行一些修复。
猜你喜欢
  • 2020-10-15
  • 2021-11-14
  • 2012-01-30
  • 1970-01-01
  • 2015-07-13
  • 2015-09-14
  • 2016-10-05
  • 2021-02-20
  • 1970-01-01
相关资源
最近更新 更多