[英]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()
实现中由锁引起的阻塞之外,还会发生以下情况:
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.