简体   繁体   English

同步到异步的异步工作有奇怪的行为

[英]Async work to sync to async has strange behavior

I have a legacy project with a lot of IoC and HttpClient calls.我有一个有很多 IoC 和HttpClient调用的遗留项目。 To improve performance, I'm trying try use TPL to parallelize the work.为了提高性能,我尝试使用 TPL 来并行化工作。 But the performance became worse.但性能变得更糟。

In summary, we try parallelize a sync method which encapsulates an async method.总之,我们尝试并行化封装异步方法的同步方法。 After refactoring, the performance is better, but I don't understands this behavior.重构后,性能更好,但我不明白这种行为。

I made this minimal code example to reproduce the behavior in .NET 4.7 console project:我制作了这个最小的代码示例来重现 .NET 4.7 控制台项目中的行为:

class Program
{
    static void Main(string[] args)
    {
        var tasks = new List<Task>();
        for (int i = 0; i < 15; i++)
        {
            var n = i;
            tasks.Add(Task.Run(() => WorkSync(n)));
            Thread.Sleep(TimeSpan.FromMilliseconds(1));
        }
        Task.WaitAll(tasks.ToArray());
    }

    private static void WorkSync(int i)
    {
        Debug.WriteLine($"{i:000}\t{DateTime.Now:HH:mm:ss.fff}\tStartA");
        WorkAsync(i).GetAwaiter().GetResult();
        Debug.WriteLine($"{i:000}\t{DateTime.Now:HH:mm:ss.fff}\tFinishA");
    }

    private static async Task WorkAsync(int i)
    {
        Debug.WriteLine($"{i:000}\t{DateTime.Now:HH:mm:ss.fff}\tStartB");
        await Task.Run(() => Work(i));
        Debug.WriteLine($"{i:000}\t{DateTime.Now:HH:mm:ss.fff}\tFinishB");
    }

    private static void Work(int i)
    {
        Debug.WriteLine($"{i:000}\t{DateTime.Now:HH:mm:ss.fff}\tDo Something");
    }
}

Result :结果 :

004 11:30:10.629    StartA
000 11:30:10.627    StartA
002 11:30:10.627    StartA
001 11:30:10.627    StartA
003 11:30:10.627    StartA
005 11:30:10.628    StartA
006 11:30:10.628    StartA
007 11:30:10.628    StartA
008 11:30:10.633    StartA
002 11:30:10.692    StartB
001 11:30:10.692    StartB
000 11:30:10.692    StartB
003 11:30:10.692    StartB
005 11:30:10.692    StartB
004 11:30:10.692    StartB
006 11:30:10.695    StartB
007 11:30:10.699    StartB
008 11:30:10.703    StartB
009 11:30:11.632    StartA
009 11:30:11.633    StartB
010 11:30:12.616    StartA
010 11:30:12.617    StartB
011 11:30:13.612    StartA
011 11:30:13.613    StartB
012 11:30:14.612    StartA
012 11:30:14.613    StartB
013 11:30:15.612    StartA
013 11:30:15.613    StartB
014 11:30:16.611    StartA
014 11:30:16.612    StartB
002 11:30:17.612    Do Something
002 11:30:17.614    FinishB
002 11:30:17.615    FinishA
001 11:30:17.615    Do Something
001 11:30:17.657    FinishB
006 11:30:17.658    Do Something
005 11:30:17.636    Do Something
006 11:30:17.680    FinishB
005 11:30:17.701    FinishB
007 11:30:17.723    Do Something
001 11:30:17.658    FinishA
005 11:30:17.744    FinishA
004 11:30:17.744    Do Something
007 11:30:17.765    FinishB
004 11:30:17.808    FinishB
006 11:30:17.723    FinishA
007 11:30:17.830    FinishA
003 11:30:17.894    Do Something
013 11:30:17.786    Do Something
003 11:30:17.895    FinishB
013 11:30:17.917    FinishB
012 11:30:17.919    Do Something
008 11:30:17.830    Do Something
014 11:30:17.788    Do Something
004 11:30:17.851    FinishA
009 11:30:17.851    Do Something
013 11:30:17.922    FinishA
000 11:30:17.872    Do Something
003 11:30:17.918    FinishA
012 11:30:17.927    FinishB
010 11:30:17.922    Do Something
008 11:30:17.931    FinishB
014 11:30:17.933    FinishB
011 11:30:17.955    Do Something
008 11:30:18.046    FinishA
009 11:30:17.958    FinishB
009 11:30:18.111    FinishA
014 11:30:18.068    FinishA
000 11:30:17.980    FinishB
000 11:30:18.114    FinishA
010 11:30:18.024    FinishB
011 11:30:18.089    FinishB
012 11:30:18.003    FinishA
011 11:30:18.138    FinishA
010 11:30:18.116    FinishA

The Work method is executed only after all tasks are started in the Main.只有在 Main 中启动了所有任务后,才会执行Work方法。 I have checked this using the debugger, it isn't the debug display that blocks.我已经使用调试器检查过这个,它不是阻塞的调试显示。

1) I don't understand this scheduling. 1)我不明白这个安排。 Can you explain why?你能解释一下为什么吗?

2) The 10 first tasks start very rapidly, but the last 5 tasks start very slowly. 2) 前 10 个任务启动非常快,但后 5 个任务启动非常慢。 Can you explain why?你能解释一下为什么吗?

I don't understand this scheduling.我不明白这个调度。 Can you explain why?你能解释一下为什么吗?

You have a tight loop launching a load of tasks.您有一个紧密的循环来启动大量任务。 They all start up, but each of them launches another thread.它们都启动了,但每个都启动了另一个线程。 That thread is not being scheduled until after the launching thread calls Debug.WriteLine($"{i:000}\\tFinishB");直到启动线程调用Debug.WriteLine($"{i:000}\\tFinishB");之后,才会调度该线程Debug.WriteLine($"{i:000}\\tFinishB"); . .

One reason why the Work() thread is being blocked is that Debug.WriteLine() acquires a lock - so if some other thread is currently writing to Debug the Work() thread will block. Work()线程被阻塞的一个原因是Debug.WriteLine()获取了一个锁——所以如果某个其他线程当前正在写入 Debug, Work()线程将被阻塞。 The moral of this is that Debug.WriteLine() can change the behaviour of multithreading, because it uses locks.这样做的寓意是Debug.WriteLine()可以改变多线程的行为,因为它使用锁。


The 10 first tasks start very speed, but the 5 last tasks start very slow.前 10 个任务启动非常快,但最后 5 个任务启动非常慢。 Can you explain why?你能解释一下为什么吗?

However, there is another more impactful reason that this is happening: The "min thread limit" for the threadpool.但是,发生这种情况还有另一个更有影响力的原因:线程池的“最小线程限制”。

The threadpool maintains a minimum number of threads ready waiting to run.线程池保持最少数量的准备好等待运行的线程。 You can see that value via the following code:您可以通过以下代码查看该值:

ThreadPool.GetMinThreads(out int workers, out int ports);
Console.WriteLine(workers);  // Prints 8 on my system.

Now the important thing to know here is that if more than the minimum number of threads are required, new ones will only be created after a delay of a few hundred milliseconds (not sure exactly how long, but it seems to be around one second).现在要知道的重要一点是,如果需要的线程数超过最小数量,则只会在几百毫秒的延迟后创建新的线程(不确定确切多长时间,但似乎在一秒左右) .

So in addition to the blocking caused by locks in the Debug.WriteLine() implementation, the following is happening:所以除了Debug.WriteLine()实现中由锁引起的阻塞之外,还会发生以下情况:

  • A load of tasks are started, consuming more than the minimum threadpool size, so a delay is introduced between new tasks after the first few tasks are started.开始加载任务,消耗的资源超过最小线程池大小,因此在前几个任务启动后,新任务之间会引入延迟。
  • Because of this delay, by the time it comes to the Work() task being started, it's being delayed.由于这种延迟,当Work()任务开始时,它被延迟了。 This causes it to start much later than it would otherwise.这会导致它比其他情况下更晚开始。

You can prove that this is happening by increasing the minimum number of threadpool threads at the start of your test code, and observing the difference in output.您可以通过在测试代码开始时增加线程池线程的最小数量并观察输出差异来证明这种情况正在发生。

To try this, add the following line of code before you launch any tasks:要尝试此操作,请在启动任何任务之前添加以下代码行:

ThreadPool.SetMinThreads(100, 100);

When I try that, all the tasks start more quickly, and some of the "Do Something" messages appear before all the other tasks are started (whereas previously those messages only appeared AFTER all the other tasks were started, as you noticed).当我尝试这样做时,所有任务启动得更快,并且一些“做某事”消息出现在所有其他任务开始之前(而以前这些消息仅在所有其他任务开始后才出现,如您所见)。

NOTE Microsoft do not recommended to change the minimum number of threads :注意Microsoft 不建议更改最小线程数

You can use the ThreadPool.SetMinThreads method to increase the minimum number of idle threads.您可以使用 ThreadPool.SetMinThreads 方法来增加空闲线程的最小数量。 However, unnecessarily increasing these values can cause performance problems.但是,不必要地增加这些值会导致性能问题。 If too many tasks start at the same time, all of them might appear to be slow.如果同时启动太多任务,所有任务都可能看起来很慢。 In most cases the thread pool will perform better with its own algorithm for allocating threads.在大多数情况下,线程池使用自己的分配线程的算法会表现得更好。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM