简体   繁体   English

在异步/等待TCP服务器中套接字断开连接后的大量时间延迟

[英]Massive time delay after socket disconnect in Async/Await TCP server

I've been upgrading some older software from the Begin/End pattern in C# to use the new async functionality of the TcpClient class. 我已经从C#中的Begin / End模式升级了一些较旧的软件,以使用TcpClient类的新async功能。

Long story short, this receive method works great for small numbers of connected sockets, and continues to work great for 10,000+ connections. 长话短说,此接收方法适用于少量已连接的套接字,并继续适用于10,000多个连接。 The problem comes when these sockets disconnect. 这些插座断开连接时会出现问题。

The method I am using server side is, in essence, this (heavily simplified but still causes the problem): 从本质上讲,我正在使用服务器端的方法(大大简化,但仍然会导致问题):

private async void ReceiveDataUntilStopped(object state)
        {
            while (IsConnected)
            {
                try
                {
                    byte[] data = new byte[8192];
                    int recvCount = await _stream.ReadAsync(data, 0, data.Length);
                    if (recvCount == 0) { throw new Exception(); }
                    Array.Resize(ref data, recvCount);
                    Console.WriteLine(">>{0}<<", Encoding.UTF8.GetString(data));
                }
                catch { Shutdown(); return; }
            }
        }

This method is called using ThreadPool.QueueUserWorkItem(ReceiveDataUntilStopped); 使用ThreadPool.QueueUserWorkItem(ReceiveDataUntilStopped);可以调用此方法ThreadPool.QueueUserWorkItem(ReceiveDataUntilStopped); when the connection is accepted. 当连接被接受时。

To test the server, I connect 1,000 sockets. 为了测试服务器,我连接了1,000个套接字。 The time it takes to accept these is neglible, around 2 seconds or so. 接受这些消息的时间很短,大约2秒钟左右。 I'm very pleased with this. 我对此感到非常高兴。 However, when I disconnect these 1,000 sockets, the process takes a substantial amount of time, 15 or more seconds, to handle the closure of these sockets (the Shutdown method). 但是,当我断开这1,000个套接字的连接时,该过程将花费大量时间(15秒或更长时间)来处理这些套接字的ShutdownShutdown方法)。 During this time, my server refuses any more connections. 在这段时间内,我的服务器拒绝了更多连接。 I emptied the contents of the Shutdown method to see if there was something in there blocking, but the delay remains the same. 我清空了Shutdown方法的内容,以查看其中是否存在阻塞,但是延迟保持不变。

Am I being stupid and doing something I shouldn't? 我是在愚蠢地做我不应该做的事吗? I'm relatively new to the async/await pattern, but enjoying it so far. 我对异步/等待模式比较陌生,但是到目前为止很喜欢。

Is this unavoidable behaviour? 这是不可避免的行为吗? I understand it's unlikely in production that 1,000 sockets will disconnect at the same time, but I'd like to be able to handle a scenario like this without causing a denial of service. 我知道在生产中不可能同时断开1,000个套接字,但是我希望能够处理这样的情况而不会导致拒绝服务。 It strikes me as odd that the listener stops accepting new sockets, but I expect this is because all the ThreadPool threads are busy shutting down the disconnected sockets? 侦听器停止接受新套接字让我感到奇怪,但是我希望这是因为所有ThreadPool线程都在忙于关闭断开的套接字?

EDIT: While I agree that throwing an exception when 0 bytes are received is not good control flow, this is not the source of the problem. 编辑:虽然我同意接收到0字节时引发异常不是很好的控制流,但这不是问题的根源。 The problem is still present with simply if (recvCount == 0) { Shutdown(); return; } if (recvCount == 0) { Shutdown(); return; } if (recvCount == 0) { Shutdown(); return; } if (recvCount == 0) { Shutdown(); return; } . if (recvCount == 0) { Shutdown(); return; } This is because ReadAsync throws an IOException if the other side disconnects uncleanly. 这是因为如果另一端断开连接不ReadAsyncReadAsync会引发IOException I'm also aware that I'm not handling the buffers properly etc. this is just an example, with minimal content, just like SO likes. 我也知道我没有正确处理缓冲区等。这只是一个示例,内容很少,就像SO一样。 I use the following code to accept clients: 我使用以下代码接受客户:

private async void AcceptClientsUntilStopped()
        {
            while (IsListening)
            {
                try
                {
                    ServerConnection newConnection = new ServerConnection(await _listener.AcceptTcpClientAsync());
                    lock (_connections) { _connections.Add(newConnection); }
                    Console.WriteLine(_connections.Count);
                }
                catch { Stop(); }
            }
        }

if (recvCount == 0) { throw new Exception(); }

In case of disconnect you throw an exception. 如果断开连接,则抛出异常。 Exceptions are very expensive. 异常非常昂贵。 I benchmarked them once at 10000/sec. 我以10000 /秒的速度对它们进行了一次基准测试。 This is very slow. 这很慢。

Under the debugger, exceptions are vastly slower again (maybe 100x). 在调试器下,异常再次变得非常慢(也许是100倍)。

This is a misuse of exceptions for control flow. 这是对控制流异常的滥用。 From a code quality standpoint this is really bad. 从代码质量的角度来看,这确实很糟糕。 Your exception handling also is really bad because it catches too much. 您的异常处理也真的很糟糕,因为它捕获了太多内容。 You meant to catch socket problems but you're also swallowing all possible bugs such as NRE. 您本打算抓住套接字问题,但同时也吞没了所有可能的错误,例如NRE。

        using (mySocket) { //whatever you are using, maybe a TcpClient
        while (true)
        {
                byte[] data = new byte[8192];
                int recvCount = await _stream.ReadAsync(data, 0, data.Length);
                if (recvCount == 0) break;
                Array.Resize(ref data, recvCount);
                Console.WriteLine(">>{0}<<", Encoding.UTF8.GetString(data));
        }
        Shutdown();
        }

Much better, wow. 好多了,哇。

Further issues: Inefficient buffer handling, broken UTF8 decoding (can't split UTF8 at any byte position!), usage of async void (probably, you should use Task.Run to initiate this method, or simply call it and discard the result task). 进一步的问题:缓冲区处理效率低下,UTF8解码中断(无法在任何字节位置拆分UTF8!),使用异步void(可能应该使用Task.Run来初始化此方法,或者简单地调用它并丢弃结果任务)。


In the comments we discovered that the following works: 在评论中,我们发现以下工作原理:

Start a high-prio thread and accept synchronously on that (no await). 启动一个高优先级线程,并在该线程上同步接受(不等待)。 That should keep the accepting going. 那应该继续接受。 Fixing the exceptions is not going to be 100% possible, but: await increases the cost of exceptions because it rethrows them. 修复异常不是100%可能的,但是:await增加了异常的代价,因为它会抛出异常。 It uses ExceptionDispatchInfo for that which holds a process-global lock while doing that. 它使用ExceptionDispatchInfo来保存进程全局锁 Might be part of your scalability problems. 可能是您的可伸缩性问题的一部分。 You could improve perf by doing await readTask.ContinueWith(_ => { }) . 您可以通过执行readTask.ContinueWith(_ => { }) That way await will never throw. 这样的等待永远不会抛出。

Based on the code provided and my initial understanding of the problem. 根据提供的代码和我对问题的初步理解。 I think that there are several things that you should do to address this issue. 我认为您应该做一些事情来解决这个问题。

  1. Use async Task instead of async void . 使用async Task而不是async void This will ensure that the async state machine knows how to actually maintain its state. 这将确保async状态机知道如何实际维护其状态。
  2. Instead of invoking ThreadPool.QueueUserWorkItem(ReceiveDataUntilStopped); 而不是调用ThreadPool.QueueUserWorkItem(ReceiveDataUntilStopped); call ReceiveDataUntilStopped via await ReceiveDataUntilStopped in the context of an async Task method. async Task方法的上下文中,通过await ReceiveDataUntilStopped调用ReceiveDataUntilStopped

With async await , the Task and Task<T> objects represent the asynchronous operation. 使用async awaitTaskTask<T>对象代表异步操作。 If you are concerned that the results of the await are executed on the original calling thread you could use .ConfigureAwait(false) to prevent capturing the current synchronization context. 如果您担心await的结果是在原始调用线程上执行的,则可以使用.ConfigureAwait(false)来防止捕获当前的同步上下文。 This is explained very well here and here too . 在这里这里也很好解释。

Additionally, look at how a similar "read-while" was written with this example . 另外,请看这个示例如何编写类似的“ read-while”。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM