.Net 本机线程池上的异步任务 运行 性能非常差

Very poor performance of async task run on threadpool in .Net native

我观察到托管代码与 .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

根据我的测试,差异出现在 optimized/non 优化代码中,包括调试版本和发布版本。

有人知道是什么导致了这个问题吗?

此问题是由于“ThreadPool”数学循环导致 GC 饥饿造成的。本质上,GC 已经决定它需要 运行(因为想要做一些互操作分配)并且它试图停止所有线程来做 collection/compaction。不幸的是,我们还没有为 .NET Native 添加劫持热循环的功能,如下所示。 Migrating Your Windows Store App to .NET Native 页面上简要提到了这一点:

Infinite looping without making a call (for example, while(true);) on any thread may bring the app to a halt. Similarly, large or infinite waits may bring the app to a halt.

解决此问题的一种方法是在您的循环中添加一个调用站点(当 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()
    {
    }

缺点是你会遇到这种看起来“丑陋”的破解方法,而且你可能会因为添加的说明而受到一些影响。我已经让这里的一些人知道,我们假设 "vanishingly rare" 的这个东西实际上被客户击中了,我们将看看可以做些什么。

感谢举报!

更新:我们围绕这种情况做了一些重大改进,将能够为 GC 劫持最长的 运行ning 线程。这些修复程序可能会在 4 月份发布的 UWP 工具的 Update 2 集中可用? (我无法控制发货时间表:-))

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