我一直在使用C#中的Begin / End模式升级某些较旧的软件,以使用async
类的新TcpClient
功能。
长话短说,这种接收方法适用于少量连接套接字,并且继续适用于10,000多个连接。当这些插座断开时会出现问题。
我使用服务器端的方法本质上是这个(大大简化但仍会导致问题):
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; }
}
}
当接受连接时,使用ThreadPool.QueueUserWorkItem(ReceiveDataUntilStopped);
调用此方法。
要测试服务器,我连接了1,000个套接字。接受这些的时间可以忽略不计,大约2秒钟左右。我对此非常满意。但是,当我断开这些1,000个套接字时,该过程需要花费大量时间(15秒或更长时间)来处理这些套接字的关闭(Shutdown
方法)。在此期间,我的服务器拒绝任何更多连接。我清空Shutdown方法的内容,看看是否有阻塞的内容,但延迟仍然相同。
我是愚蠢的做某事我不应该吗?我对async / await模式比较新,但到目前为止还很享受。
这是不可避免的行为吗?我知道在生产中不太可能同时断开1000个插槽,但我希望能够在不导致拒绝服务的情况下处理这样的情况。听起来很奇怪听众停止接受新套接字,但我希望这是因为所有ThreadPool线程都在忙着关闭断开连接的套接字?
编辑:虽然我同意在收到0字节时抛出异常并不是一个好的控制流程,但不是问题的根源。简单if (recvCount == 0) { Shutdown(); return; }
仍然存在问题。这是因为如果另一方不清楚地断开连接,ReadAsync
会抛出IOException
。我也意识到我没有正确处理缓冲区等等。这只是一个例子,内容很少,就像SO喜欢的那样。我使用以下代码接受客户:
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(); }
}
}
答案 0 :(得分:2)
if (recvCount == 0) { throw new Exception(); }
如果断开连接,则抛出异常。例外是非常昂贵的。我以10000 /秒的速度对它们进行一次基准测试。这很慢。
在调试器下,异常再次大大减慢(可能是100倍)。
这是对控制流的异常的滥用。从代码质量的角度来看,这非常糟糕。您的异常处理也非常糟糕,因为它捕获的太多了。您意味着来解决套接字问题,但您也会吞下所有可能的错误,例如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();
}
好多了,哇。
其他问题:缓冲区处理效率低,UTF8解码失败(无法在任何字节位置拆分UTF8!),使用async void(可能应该使用Task.Run
启动此方法,或者只是调用它并丢弃结果任务)。
在评论中我们发现以下工作:
启动一个高prio线程并同步接受(无等待)。这应该保持接受。修复异常不是100%可能的,但是:await会增加异常的成本,因为它会重新抛出异常。它使用ExceptionDispatchInfo
来保存进程全局锁。可能是您的可伸缩性问题的一部分。您可以通过等待readTask.ContinueWith(_ => { })
来提高性能。等待永远不会抛出。
答案 1 :(得分:0)
基于提供的代码和我对问题的初步了解。我认为你应该采取一些措施来解决这个问题。
async Task
代替async void
。这将确保async
状态机知道如何实际维护其状态。ThreadPool.QueueUserWorkItem(ReceiveDataUntilStopped);
方法的上下文中通过ReceiveDataUntilStopped
调用await ReceiveDataUntilStopped
来电async Task
。使用async
await
,Task
和Task<T>
个对象代表异步操作。如果您担心在原始调用线程上执行await
的结果,则可以使用.ConfigureAwait(false)
来阻止捕获当前的同步上下文。这个问题解释得非常好here和here too。
此外,看一下类似的&#34; read-while&#34;写得with this example。