为什么这个xunit测试死锁(在单个cpu VM上)?

时间:2018-06-18 23:15:05

标签: async-await .net-core xunit

在单个CPU VM(Ubuntu 18.4)上运行以下测试

using System;
using System.Threading;
using System.Threading.Tasks;
using Xunit;

public class AsyncStuffTest
{
    [Fact]
    public void AsyncTest()
    {
        SomethingAsync().Wait();
    }

    private static async Task SomethingAsync()
    {
        Console.WriteLine("before async loop...");
        await Task.Factory.StartNew(() => {
                                        for (int i = 0; i < 10; i++)
                                        {
                                            Console.WriteLine("in async loop...");
                                            Thread.Sleep(500);     
                                        }
                                    });
        Console.WriteLine("after async loop...");
    }
}

结果如下:

Build started, please wait...
Build completed.

Test run for /home/agent/fancypants/bin/Debug/netcoreapp2.1/fancypants.dll(.NETCoreApp,Version=v2.1)
Microsoft (R) Test Execution Command Line Tool Version 15.7.0
Copyright (c) Microsoft Corporation.  All rights reserved.

Starting test execution, please wait...
before async loop...
in async loop...
in async loop...
in async loop...
in async loop...
in async loop...
in async loop...
in async loop...
in async loop...
in async loop...
in async loop...

该过程似乎陷入僵局,永远不会进入预期的输出after async loop...

在我的开发机器上运行一切正常。

注意:我知道在xunit中进行异步测试的可能性。这或多或少是一个有趣的问题。特别是因为这个问题只影响xunit,控制台应用程序终止正常:

~/fancypants2$ dotnet run
before async loop...
in async loop...
in async loop...
in async loop...
in async loop...
in async loop...
in async loop...
in async loop...
in async loop...
in async loop...
in async loop...
after async loop...
~/fancypants2$

更新: 阅读有关xunit中async的最新修补程序,所以我尝试使用2.4.0-beta.2.build4010,但没有变化。

1 个答案:

答案 0 :(得分:2)

经过两天的思考,SynchronizationContext(基本上可以在不讨论“ UI线程”的情况下找到最好的信息:https://blogs.msdn.microsoft.com/pfxteam/2012/01/20/await-synchronizationcontext-and-console-apps/),我了解发生了什么。

控制台应用程序不提供任何SynchronizationContext,因此CLR将把任务卸载到线程池上的线程中。无论计算机使用什么CPU,都有足够的可用线程。一切正常。

xunit确实提供了Xunit.Sdk.MaxConcurrencySyncContext,可以主动管理正在运行的线程数量。默认情况下,最大并发级别默认为您拥有的逻辑CPU数量,但是可以为configured。运行测试的线程已经超出了此限制,因此任务完成受阻。

所有这些都是为了重现一个复杂得多的ASP.Net Core Web应用程序的问题,该问题在提到的单个CPU构建代理上表现得很奇怪。集成测试使用集合范围广泛的共享夹具,该夹具启动TestServer

public class ServiceHostFixture : IAsyncLifetime
{
    public async Task InitializeAsync()
    {
        IWebHostBuilder host = new WebHostBuilder()
                    .UseEnvironment("Production")
                    .UseStartup<Startup>();

        Server = new TestServer(host);
    }

    public async Task DisposeAsync()
    {
        Server.Dispose();
    }
}

Startup.Configure(IApplicationBuilder app)中有一个有趣的地方:

app.ApplicationServices
    .GetRequiredService<IApplicationLifetime>()
    .ApplicationStarted
    .Register(async () => {
                    try
                    {
                        // it blocks here in xunit
                        await EnsureSomeBasicStuffExistenceInTheDatabaseAsync();
                    }
                    catch (Exception ex)
                    {
                        Logger.Fatal(ex, "Application could not be started");
                    }
                });

在我的(8个逻辑CPU)计算机上,它可以正常工作,在单个cpu Web主机上它可以正常工作,但是在单个cpu死锁上可以运行xunit。如果您仔细阅读CancellationToken实际上是ApplicationStarted的文档,则会发现:

  

当前System.Threading.ExecutionContext(如果存在)将与委托一起捕获,并在执行时使用。

将其与ASP.Net Core和xunit之间的区别相结合就揭示了这个问题。我所做的是以下解决方法:

app.ApplicationServices
    .GetRequiredService<IApplicationLifetime>()
    .ApplicationStarted
    .Register(async () => {
                    try
                    {
                        if (SynchronizationContext.Current == null)
                        {
                            // normal ASP.Net Core environment does not have a synchronization context, 
                            // no problem with await here, it will be executed on the thread pool
                            await EnsureSomeBasicStuffExistenceInTheDatabaseAsync;
                        }
                        else
                        {
                            // xunit uses it's own SynchronizationContext that allows a maximum thread count
                            // equal to the logical cpu count (that is 1 on our single cpu build agents). So
                            // when we're trying to await something here, the task get's scheduled to xunit's 
                            // synchronization context, which is already at it's limit running the test thread
                            // so we end up in a deadlock here.
                            // solution is to run the await explicitly on the thread pool by using Task.Run
                            Task.Run(() => EnsureSomeBasicStuffExistenceInTheDatabaseAsync()).Wait();
                        }
                    }
                    catch (Exception ex)
                    {
                        Logger.Fatal(ex, "Application could not be started");
                    }
                });