在下面的示例中,是什么让同步和异步任务的混合如此缓慢?

What makes mixing of sync and async Tasks so terrible slow in the following example?

我 运行 在(无意中)将异步任务与 syn 任务混合时遇到了大问题:以下示例是原始问题的压缩版本。

平台是 Windows10,Microsoft.NET.Sdk.Web,2 个内核,4 个逻辑处理器 @ 2.4 GHz

这段代码代表了原始问题:它执行一个同步任务和一个异步任务每 20 次:

var sema = new SemaphoreSlim(1);

var tasks = new List<Task>();

for (int i = 0; i < 20; i++)
{
    var t2 = Task.Run(async () =>
    {
        var sw = new Stopwatch();
        sw.Start();
        await sema.WaitAsync().ConfigureAwait(false);
        try
        {
            await Task.Delay(1).ConfigureAwait(false);
        }
        finally
        {
            sema.Release();
        }
        sw.Stop();
        Console.WriteLine($"sync {sw.Elapsed}");
    });

    var t1 = Task.Run(() =>
    {
        var sw = new Stopwatch();
        sw.Start();
        sema.Wait();
        try
        {
        }
        finally
        {
            sema.Release();
        }
        sw.Stop();
        Console.WriteLine($"async {sw.Elapsed}");
    });

    tasks.Add(t1);
    tasks.Add(t2);
}

await Task.WhenAll(tasks).ConfigureAwait(false);

大约需要16s完成。值得注意的是第一次同步是在 800 毫秒之后。

sync 00:00:00.8306484
sync 00:00:16.8401071
sync 00:00:16.8559379
sync 00:00:16.8697014
async 00:00:16.8697706
async 00:00:16.8699273
async 00:00:16.8710140
async 00:00:16.8710523
sync 00:00:16.0248058
async 00:00:16.0246810
sync 00:00:15.0783237
async 00:00:15.0782280
sync 00:00:14.5762648
async 00:00:14.5760971
sync 00:00:13.5689368
async 00:00:13.5591823
sync 00:00:12.6271075
async 00:00:12.6270483
sync 00:00:11.6318846
async 00:00:11.6317560
sync 00:00:10.6406636
async 00:00:10.6404542
sync 00:00:09.1580280
async 00:00:09.1574764
sync 00:00:08.1862783
async 00:00:08.1860869
sync 00:00:07.2034033
async 00:00:07.2032430
sync 00:00:06.2139071
async 00:00:06.2136905
sync 00:00:05.2354887
async 00:00:05.2353404
sync 00:00:04.2503136
async 00:00:04.2501821
sync 00:00:03.2656311
async 00:00:03.2655521
sync 00:00:02.2806897
async 00:00:02.2805796
sync 00:00:01.2974060
async 00:00:01.2972398

相比之下,以下代码每运行 20 次异步任务:

var sema = new SemaphoreSlim(1);

var tasks = new List<Task>();

for (int i = 0; i < 20; i++)
{
    var t2 = Task.Run(async () =>
    {
        var sw = new Stopwatch();
        sw.Start();
        await sema.WaitAsync().ConfigureAwait(false);
        try
        {
            await Task.Delay(1).ConfigureAwait(false);
        }
        finally
        {
            sema.Release();
        }
        sw.Stop();
        Console.WriteLine($"sync {sw.Elapsed}");
    });

    var t1 = Task.Run(async () =>
    {
        var sw = new Stopwatch();
        sw.Start();
        await sema.WaitAsync().ConfigureAwait(false);
        try
        {
        }
        finally
        {
            sema.Release();
        }
        sw.Stop();
        Console.WriteLine($"async {sw.Elapsed}");
    });

    tasks.Add(t1);
    tasks.Add(t2);
}

await Task.WhenAll(tasks).ConfigureAwait(false);

只需300ms即可完成:

async 00:00:00.0180861
sync 00:00:00.0329542
async 00:00:00.0181292
sync 00:00:00.0177771
async 00:00:00.0432851
sync 00:00:00.0476872
sync 00:00:00.0635321
sync 00:00:00.0774490
async 00:00:00.0775557
async 00:00:00.0775724
async 00:00:00.0775398
sync 00:00:00.0942652
sync 00:00:00.1082544
async 00:00:00.1080930
sync 00:00:00.1240859
async 00:00:00.1246952
sync 00:00:00.1397922
async 00:00:00.1414005
sync 00:00:00.1547058
async 00:00:00.1546395
sync 00:00:00.1705435
async 00:00:00.1705003
sync 00:00:00.1863422
async 00:00:00.1865136
sync 00:00:00.2052246
async 00:00:00.2054538
sync 00:00:00.2172049
async 00:00:00.2171460
sync 00:00:00.2330110
async 00:00:00.2329556
sync 00:00:00.2489691
async 00:00:00.2492344
sync 00:00:00.2647401
async 00:00:00.2645481
async 00:00:00.2645736
sync 00:00:00.2785660
async 00:00:00.2785652
sync 00:00:00.2944074
sync 00:00:00.3116578
async 00:00:00.3184570

我知道,应该避免混合异步和同步。但是第一个变体出现巨大延迟的原因是什么?我无法想象这 16 秒内发生了什么,这几乎是 CPU.

的“无限时间”

而且,为什么连第一种情况的第一条消息都过了800ms:这个时间本身就已经很意外了。

线程池专为 运行 快速操作而设计。您的第一个程序将大量工作安排到 运行 的线程池中,持续了很长一段时间(因为您已经安排了大量只能 运行 顺序执行的操作,所以它们都在互相等待),你安排的工作比工作人员多,所以你最终会遇到这样的情况,即每个工作人员都只是坐在那里等待线程池队列中更靠后的其他工作。在这种情况下,您 基本上 生成了 the most common async deadlock situation,因为您阻止了调度程序 运行 使工作完成所需的继续,只是它没有技术上死锁,因为线程池会注意到没有工作正在完成,并随着时间的推移添加更多的工作人员,每个工作人员将只是坐在那里什么都不做,等待工作队列中更靠后的事情最终被安排。 最终 您最终拥有足够的线程池线程,可以实际继续工作。但是,直到线程池最终创建了与您为其安排的工作一样多的线程后,工作才会继续,正如您所见,这需要一些时间。

当你异步完成整个事情时,你不会遇到阻止调度程序做更多工作的异步同步问题,因为你让线程池做的工作只是实际工作需要完成,而不是让工人坐在那里等待其他事情完成。