我发现了一种我无法理解的奇怪行为。
下面的代码是问题的简化。私有异步调用方法 Listen() 通常是在 Test 中调用的对象的 Method,然后在 Test 的 Dispose 中处置该对象。这里我把它放在内部,以减少代码的大小。
如果我运行测试,完成后就会死锁,并且永远不会进入TestDeadLock.Dispose()。
如果我在测试中执行 using() ,则不会有任何问题,因为套接字将被释放并且异步调用将被删除。但是我想了解情况,并且我更喜欢分解 Dispose() 和对象实例化。
using Xunit;
using System.Net;
using System;
using System.Net.Sockets;
using System.Threading.Tasks;
#if NET6_0_OR_GREATER
namespace Tests
{
// Avoid paralellizing socket tests
[Collection("SocketTests")]
public class TestDeadlock : IDisposable
{
private readonly Socket _socket;
private bool _disposed;
public TestDeadlock()
{
_socket = new Socket(SocketType.Stream, ProtocolType.IP);
_socket.Bind(new IPEndPoint(IPAddress.Loopback, 10000));
_socket.Listen();
}
// in my code this method is a method of the Object that is instanced and disposed by the test lib
private async void Listen()
{
var client = await _socket.AcceptAsync();
}
[Fact]
public void GoDeadLock()
{
Listen();
Assert.True(true);
}
// this does not dead locks the test
private async Task ListenNoDeadLock()
{
var client = await _socket.AcceptAsync();
}
[Fact]
public void WorkButGivesWarning()
{
ListenNoDeadLock(); // Task not awaited => warning
Assert.True(true);
}
protected virtual void Dispose(bool disposing)
{
// This is never reached. XUnits blocks just after exiting the Fact
if (!_disposed)
{
if (disposing)
{
_socket.Dispose();
}
_disposed = true;
}
}
public void Dispose()
{
// Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}
#endif
但是,如果我将 Listen 方法从 async void 更改为 async Task (并且仍然触发并忘记它)...它可以工作!
但我不希望这个方法返回任务,因为我希望它只是在后台启动一个异步操作。
我知道我可以在监听中执行一个 Task.Run ,但我觉得它会使用一个线程来到达等待并放弃它。
您能解释一下并提供一些关于实现此目的的干净方法的建议吗?
编辑: 我想我找到了一种干净的方法来实现“Fire and Forget”、处理异常并且不会导致 XUnit 死锁:
// better solution
private void GoodFireAndForget()
{
Task.Run( async () =>
{
var client = await _socket.AcceptAsync();
}).ContinueWith((task) =>
{
task.Exception.Handle((e) =>
{
// handle exceptions
return true;
});
}, TaskContinuationOptions.OnlyOnFaulted);
}
[Fact]
public void WorksAndSeemsTheGoodWay()
{
GoodFireAndForget();
Assert.True(true);
}
我想我对 XUnit 死锁有了解释,感谢 https://www.damirscorner.com/blog/posts/20220415-UsingAsyncVoidInXunitTests.html
根据此链接:
[XUnit] 得益于其自定义同步上下文,可以正确处理具有 async void 签名的异步测试。除了代码中的摘要注释之外,我找不到任何相关文档:
“SynchronizationContext 的这种实现允许开发人员跟踪未完成的 async void 操作的计数,并等待它们全部完成。”
所以看起来 XUnit 在幕后正在同步等待所有 Async Void 方法完成。我们知道有时可能会陷入僵局。因此,测试库中存在的任何 Async Void 都可能阻止 Xunit。我猜由于任务对象,异步任务被取消了(即使我不明白如何取消)。