如果我触发并忘记 async void 方法中的 socket.Listen() ,为什么 XUnit 会死锁?

问题描述 投票:0回答:1

我发现了一种我无法理解的奇怪行为。

下面的代码是问题的简化。私有异步调用方法 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);
    }
c# sockets async-await deadlock xunit
1个回答
0
投票

我想我对 XUnit 死锁有了解释,感谢 https://www.damirscorner.com/blog/posts/20220415-UsingAsyncVoidInXunitTests.html

根据此链接:

[XUnit] 得益于其自定义同步上下文,可以正确处理具有 async void 签名的异步测试。除了代码中的摘要注释之外,我找不到任何相关文档:

“SynchronizationContext 的这种实现允许开发人员跟踪未完成的 async void 操作的计数,并等待它们全部完成。”

所以看起来 XUnit 在幕后正在同步等待所有 Async Void 方法完成。我们知道有时可能会陷入僵局。因此,测试库中存在的任何 Async Void 都可能阻止 Xunit。我猜由于任务对象,异步任务被取消了(即使我不明白如何取消)。

© www.soinside.com 2019 - 2024. All rights reserved.