死锁与xUnit和异步

huangapple go评论64阅读模式
英文:

Deadlock with xUnit and async

问题

我在某些机器上使用这个xUnit测试时出现了死锁。

不幸的是,这是缩短的代码,由于问题似乎是随机发生的,所以无法使用这段代码复现。但我希望我已经包含了所有重要的内容。

一个转储显示测试被卡住了,分析告诉我:

1个线程正在方法 'DataHandler.LogSpecialOperation(...)' 中执行异步操作,但正在等待同步调用方法 'System.Threading.ManualResetEventSlim.Wait(int, System.Threading.CancellationToken)'。这可能导致线程池饥饿和挂起。

测试和类

PS:我有多个类似的测试。也许它们相互阻塞。

[Fact]
public void TestDataHandlerLogging()
{
    var logger = new StubLogger<TestClass>();
    var fileWriter = new StubDumpFileWriter(); // 由于这是一个存根:不执行特殊操作,没有文件系统访问,...

    var dataHandler = new DataHandler(logger, fileWriter);
    dataHandler.LogSpecialOperation(...);

    ...
}
public class DataHandler
{
    private readonly object specialOperationLock = new(); // 不被 StubDumpFileWriter 使用,所以不应该导致测试中的死锁

    internal void LogSpecialOperation(...)
    {
        CsvDataLogger.LogCsvToFileAsync(specialOperationLock, ..., logger, dumpFileWriter)
            .GetAwaiter().GetResult();
    }
}
public class CsvDataLogger
{
    public static async Task LogCsvToFileAsync(object lockObject, ..., ILogger logger, IDumpFileWriter dumpFileWriter)
    {
        await Task.Run(() =>
        {
            dumpFileWriter.WriteCsv(lockObject, ..., logger); // 由于这是一个存根,这里不应该发生重要的事情
        });
    }
}

让我困惑的是分析中的 ManualResetEventSlim.Wait。我认为我在任何地方都没有使用它。它是来自xUnit吗?在使用xUnit时是否需要考虑什么?

英文:

I have deadlocks on just some machines using this xUnit test.

Unfortunately this is shortened code and as the problem seems to accur randomly it is not reproducable with this code. But I hope I got everthing that is important.

A dump showed me the test it stuck. and an analysis tells me this:

1 threads are performing asynchronous work in method &#39;DataHandler.LogSpecialOperation(...)&#39;, but are waiting on a synchronous call to method &#39;System.Threading.ManualResetEventSlim.Wait(int, System.Threading.CancellationToken)&#39;. This may cause thread pool starvation and hangs.

The test and the classes
PS: I have more than one test similar to this. Maybe they block each other.

[Fact]
public void TestDataHandlerLogging()
{
	var logger = new StubLogger&lt;TestClass&gt;();
	var fileWriter = new StubDumpFileWriter(); // as this is a stub: does nothing special, no file system access, ...

	var dataHandler = new DataHandler(logger, fileWriter);
	dataHandler.LogSpecialOperation(...);

	...
}
public class DataHandler
{
	private readonly object specialOperationLock = new(); // not used by the StubDumpFileWriter so should not be responsible for deadlocks in tests

	internal void LogSpecialOperation(...)
	{
		CsvDataLogger.LogCsvToFileAsync(specialOperationLock, ..., logger, dumpFileWriter)
            .GetAwaiter().GetResult();
	}
}
public class CsvDataLogger
{
    public static async Task LogCsvToFileAsync(object lockObject, ..., ILogger logger, IDumpFileWriter dumpFileWriter)
    {
        await Task.Run(() =&gt;
        {
            dumpFileWriter.WriteCsv(lockObject, ..., logger); // as this is a stub, nothing important should happen here
        });
    }
}

What confuses me is ManualResetEventSlim.Wait in the analysis. I think I don't use it anywhere. Is it from xUnit? Is there something I have to consider when using xUnit?

答案1

得分: 2

根据Marc的怀疑:

xUnit确实对其所有的测试应用了一种一次只能运行一个线程的同步上下文。并非所有测试框架都这样做;旧版本的测试框架更注重向后兼容性 - MSTest从不提供上下文,而NUnit有时提供有时不提供。但xUnit在async出现时相对较新,因此它为每个测试都创建了一个上下文。

使用一次只能运行一个线程的上下文,像您发布的那种阻塞代码可能会导致死锁(如我在我的博客上所描述的)。这是因为await的行为方式不同,它会在其可等待项(任务)已经完成的情况下表现得同步(如我在我的博客中所描述的) - 因此,如果等待的代码完成得太快,就不会发生死锁。

如果“DataHandler”和ILogger意味着ASP.NET Core,那么在运行时,这种类型的死锁不会发生,因为ASP.NET Core没有一次只能运行一个线程的上下文。但阻塞仍然是一个不好的主意(如我在我的博客中所描述的),因为这会浪费一个线程。

我必须重申另一位评论者的看法:

有几个高质量、经过充分测试的日志框架已经妥善解决了这个问题。通常的方法是(同步地)将日志消息放入队列,并定期从后台线程将这些日志消息刷新到存储中。这样,您的日志语句不必在整个应用程序中都是异步的。

英文:

As Marc suspected:

> if I was writing a testing framework, I'd intentionally design it to have such a sync context, specifically to help find incorrect usage like this

xUnit does have a one-thread-at-a-time synchronization context applied to all its tests. Not all frameworks do; older ones have prioritized backwards compatibility - MSTest never provides a context and NUnit sometimes does and sometimes doesn't. But xUnit was new enough when async came out that they placed a context on every test.

With any one-thread-at-a-time context, blocking code like what you posted could cause deadlocks (as described on my blog). It happens "randomly" because of how await behaves; it can behave synchronously (as described on my blog) if its awaitable (task) is already complete - so the deadlock will not occur if the awaited code completes too quickly.

If "DataHandler" and ILogger imply ASP.NET Core, then at runtime this kind of deadlock cannot occur because ASP.NET Core does not have a one-thread-at-a-time context. But it's still a bad idea to block anyway (as described on my blog), because it wastes a thread unnecessarily.

> I think I'll get rid of the GetAwaiter().GetResult(). Takes a bit of time but hopefully is worth it.

I must echo the sentiment of another commenter:

> This has been solved in X Logging Frameworks. Why not use them?

There are several high-quality, heavily tested logging frameworks that have solved this problem appropriately. The usual approach is to (synchronously) place log messages into a queue and occasionally flush those log messages to storage from a background thread. This way, your log statements don't have to be asynchronous all throughout your application.

huangapple
  • 本文由 发表于 2023年2月27日 18:12:28
  • 转载请务必保留本文链接:https://go.coder-hub.com/75579128.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定