我刚刚在我的应用程序中发现了与StreamWriter
和StreamReader
的创建相关的性能问题。我一直在测试一个非常简单的应用程序的性能,这些测试已经在同一台机器上本地完成了几次。
客户端应用程序,尝试创建4000个连接并每200ms发送一条消息。服务器是一个接受连接的ECHO服务,并返回输入。
使用此代码in the server to handle the socket connection and return data:
StreamReader sr = null;
StreamWriter sw = null;
try
{
var stream = client.GetStream();
sr = new StreamReader(stream, Encoding.UTF8);
sw = new StreamWriter(stream, Encoding.UTF8);
while (!cancel.IsCancellationRequested && client.Connected)
{
var msg = await sr.ReadLineAsync(); ;
if (msg == null)
continue;
_inMessages.Increment();
_inBytes.IncrementBy(msg.Length);
await sw.WriteLineAsync(msg);
await sw.FlushAsync();
_outMessages.Increment();
_outBytes.IncrementBy(msg.Length);
}
}
catch (Exception aex)
{
var ex = aex.GetBaseException();
Console.WriteLine("Client error: " + ex.Message);
}
finally
{
_connected.Decrement();
if(sr != null)
sr.Dispose();
if(sw != null)
sw.Dispose();
}
允许非常快速地连接4000个客户端,使用28-30%的CPU每秒处理大约14000(14千)条消息。
另一方面,使用此代码:
StreamReader sr = null;
StreamWriter sw = null;
try
{
var stream = client.GetStream();
while (!cancel.IsCancellationRequested && client.Connected)
{
sr = new StreamReader(stream, Encoding.UTF8); // moved
sw = new StreamWriter(stream, Encoding.UTF8); // moved
var msg = await sr.ReadLineAsync(); ;
if (msg == null)
continue;
_inMessages.Increment();
_inBytes.IncrementBy(msg.Length);
await sw.WriteLineAsync(msg);
await sw.FlushAsync();
_outMessages.Increment();
_outBytes.IncrementBy(msg.Length);
}
}
catch (Exception aex)
{
var ex = aex.GetBaseException();
Console.WriteLine("Client error: " + ex.Message);
}
finally
{
_connected.Decrement();
if(sr != null)
sr.Dispose();
if(sw != null)
sw.Dispose();
}
允许连接4000个客户端,但最后500个客户端需要一段时间才能连接。使用30-32%的CPU,每秒处理大约6000条消息。
在两次测试中,大约有20%-30%的CPU可用且RAM内存充足。
我知道在循环中创建对象效率不高,但这种影响太大了,我想了解这里发生了什么。如果在第二个代码段中,我将sr
和sw
放在using
语句上,更糟糕的是,只有1500个客户端可以连接,并且每秒只处理大约1000条消息,可能是因为StreamReader
和StreamWriter
正在处理(或试图处置)基础NetworkStream
。
仅仅因为StreamReader
和StreamWriter
对象分配,性能是否会降低很多?或者那些特定的类还有其他东西吗?
完整代码可在此处找到:https://github.com/vtortola/AynchronousTCPListener
在实际代码中,直到我读取Stream
(帧头)的第一个字节,我不知道信息是二进制还是文本,所以我不能在手工创建读写器之前。基本上我得到了头,然后我可以决定如何处理消息。什么是更好的方法?
更新
我已启用两个性能计数器来监视服务器应用程序的线程数。我让它跑了5分钟,这些是我得到的数字:
使用第一个代码段(快速代码段):
# of current logical Threads: 108
# of current physical Threads: 106
使用第二个代码段(慢速代码段):
# of current logical Threads: 22
# of current physical Threads: 20
这解释了为什么性能下降,但为什么这会在线程中产生如此大的影响呢?
此外,第一种情况下的内存使用量约为120Mb,而第二种情况下320Mb的内存使用量增加了266%。
答案 0 :(得分:5)
第二个版本存在一些问题,而较慢的消息处理只是冰山一角。让我们开始吧。
你是对的,创建对象不应该是一个问题,它不是,但在内存中管理它们。如果将GC性能计数器添加到性能监视器,您会发现垃圾收集量急剧增长。看看下面的2张图片:
首先(正确)案例:
第二(错误)案例:
CPU花在垃圾收集上的时间要高得多,从而为代码留下更少的宝贵CPU时间。此外,正如您所指出的,在第二种情况下,内存使用率远高于第一种情况。要理解这一点,您需要了解如何构建GC堆。基本上有3个称为代的内存段(还有大对象堆,但在我们的情况下它不感兴趣):
回到您的应用程序。当您在每个循环中创建StreamReader
或StreamWriter
类时,您正在快速耗尽Gen0可用空间,从而迫使GC收集此段中的内存。流对象不会立即处理,因为异步任务可能会保留对它们的引用。因此,它们被移动到Gen1段,再次耗尽它并导致GC执行Gen1垃圾收集。最后,他们要么通过Getn1垃圾收集处理,要么在Gen2中登陆。正如我之前所说的,当存储在其中的对象未被处理时Gen2的大小增加,这解释了第二种情况下更高的内存使用量。由于GC不愿意执行Gen2集合,您的网络流(由读取器和写入程序使用)不会快速处理,从而允许您的服务器接收客户端消息。我们正在慢慢转向下一个点:
当您创建读者和编写者时,您正在使用构造函数来强制他们在处置时关闭底层流。这意味着您无法控制何时关闭客户端的网络流。这就是为什么您可能会在客户端观察到许多连接丢失和重试的原因。对于这种情况,更适合的构造函数是:
sr = new StreamReader(stream, Encoding.UTF8, true, 1024, true);
sw = new StreamWriter(stream, Encoding.UTF8, 1024, true);
将未连接的连接打开,让你负责处理它(你应该这样做)。最后,继续使用第一个版本,但更改读者和编写者的构造函数,并将client.Dispose
添加到finally块:)