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



我有一个遗留项目,有很多 IoC 和HttpClient调用。为了提高性能,我正在尝试使用 TPL 来并行化工作。但表现变得更糟。

总之,我们尝试并行化封装异步方法的同步方法。 重构后,性能更好,但我不理解这种行为。

我制作了这个最小的代码示例来重现 .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");
}
}

结果:

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

只有在 Main 中启动所有任务后,才会执行Work方法。 我已经使用调试器检查了这一点,阻止的不是调试显示。

1(我不明白这个调度。 你能解释一下为什么吗?

2(前 10 个任务开始得非常快,但最后 5 个任务开始得很慢。 你能解释一下为什么吗?

我不明白这个调度。你能解释一下为什么吗?

你有一个紧密的循环来启动大量任务。它们都启动了,但每个线程都启动了另一个线程。直到启动线程调用Debug.WriteLine($"{i:000}tFinishB");之后才会调度该线程。

Work()线程被阻塞的一个原因是Debug.WriteLine()获取了一个锁 - 因此,如果其他线程当前正在写入调试,则Work()线程将阻塞。这样做的寓意是Debug.WriteLine()可以改变多线程的行为,因为它使用锁。

前 10 个任务开始得

非常快,但最后 5 个任务开始得很慢。你能解释一下为什么吗?

但是,发生这种情况还有另一个更有影响力的原因:线程池的"最小线程限制"。

线程池保持准备等待运行的最小线程数。您可以通过以下代码查看该值:

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

现在这里要知道的重要一点是,如果需要的线程数超过最小数量,则只会在延迟几百毫秒后创建新线程(不确定确切多长时间,但似乎大约一秒(。

因此,除了Debug.WriteLine()实现中的锁造成的阻塞之外,还发生了以下情况:

  • 启动的任务负载超过最小线程池大小,因此在启动前几个任务后,新任务之间会引入延迟。
  • 由于这种延迟,当涉及到正在启动的Work()任务时,它被延迟了。这会导致它比其他方式晚得多。

您可以通过在测试代码开始时增加最小线程池线程数并观察输出的差异来证明这种情况正在发生。

若要尝试此操作,请在启动任何任务之前添加以下代码行:

ThreadPool.SetMinThreads(100, 100);

当我尝试这样做时,所有任务的启动速度都更快,并且一些"做某事"消息在所有其他任务启动之前出现(而以前这些消息仅在所有其他任务启动后出现,正如您所注意到的(。

注意Microsoft 不建议更改最小线程数:

可以使用 ThreadPool.SetMinThreads 方法来增加空闲线程的最小数目。但是,不必要地增加这些值可能会导致性能问题。如果同时启动的任务太多,则所有任务可能看起来都很慢。在大多数情况下,线程池使用自己的分配线程算法会表现得更好。

最新更新