在 .Net 本机中的线程池上运行的异步任务的性能非常差 [英] Very poor performance of async task run on threadpool in .Net native

查看:23
本文介绍了在 .Net 本机中的线程池上运行的异步任务的性能非常差的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我观察到托管与 .Net 本机代码之间存在奇怪的差异.我有一项繁重的工作被重定向到线程池.在托管代码中运行应用程序时,一切正常,但是一旦我打开本机编译 - 任务运行速度慢了几倍,速度太慢以至于它挂起 UI 线程(我猜 CPU 太过载了).

I've observed a strange difference in managed vs .Net native code. I've a heavy job redirected to threadpool. When running the app in managed code, everything works smooth but as soon as I switch on native compilation - the task run few times slower and so slow that it hangs UI thread (I guess CPU is so overloaded).

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

Here are two screenshots from debug output, the one on the left is from managed code, and the one on the right is from native compilation. As you can see the time consumed by UI task is nearly the same in both cases, up to a time when threadpool job is started - then in managed version UI elapsed time grows (in fact UI gets blocked and you cannot take any action). Timings of threadpool job speak for themselves.

重现问题的示例代码:

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");
}

如果你需要一个完整的样本 - 那么你可以在此处下载.

If you need a complete sample - then you can download it here.

正如我所测试的,差异出现在优化/非优化代码上,在调试和发布版本中.

As I've tested the difference appears on both optimized/non optimized code, in both debug and release versions.

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

Does anybody have an idea what can cause the problem?

推荐答案

这个问题是因为ThreadPool"数学循环导致了 GC 饥饿.本质上,GC 已经决定它需要运行(因为想要进行一些互操作分配)并且它试图停止所有线程来进行收集/压缩.不幸的是,我们还没有添加 .NET Native 劫持热循环的功能,就像你在下面看到的那样.这在迁移您的 Windows 应用商店中简要提及应用到 .NET Native 页面为:

This issue is caused because the "ThreadPool" math loop is causing GC starvation. Essentially, the GC has decided that it needs to run (due to wanting to do some interop allocation) and it’s trying to stop all of the threads to do collection/compaction. Unfortunately, we haven’t added the ability for .NET Native to hijack hot loops like the one you have below. This is briefly mentioned on Migrating Your Windows Store App to .NET Native page as:

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

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 很乐意在尝试调用另一个方法时中断您的线程!).

One way to work around this is to add a call site into your loop (the GC is very happy to interrupt your thread when it’s trying to call another method!).

    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,并且你可能会因为添加的说明而受到影响.我已经让这里的一些人知道,我们认为非常罕见"的东西实际上被客户击中了,我们会看看可以做些什么.

The downside is that you’ll have this "ugly" looking hack and you may suffer a bit from the added instructions. I've let some folks here know that this thing that we assumed was "vanishingly rare" is actually hit by a customer and we'll see what can be done about it.

感谢报告!

更新:我们围绕这个场景做了一些重大改进,将能够劫持大部分长时间运行的线程以进行 GC.这些修复程序可能会在 4 月发布的更新 2 版 UWP 工具中提供?(我不控制发货时间表:-))

Update: We have made some big improvements around this scenario and will be able to hijack most long running threads for GC. These fixes will be available in the Update 2 set of UWP tools out probably in April? (I don't control the shipping schedule :-) )

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

Update update: The new tools are now available as part of UWP tools 1.3.1. We don't expect to have a perfect solution to threads aggressively fighting against being hijacked by the GC but I expect this scenario to be much better with the latest tools. Let us know!

这篇关于在 .Net 本机中的线程池上运行的异步任务的性能非常差的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆