比方说,我有一个静态列表List<string> dataQueue
,其中数据以随机间隔和变化速率不断添加(1-1000个条目/秒)。
我的主要目标是将数据从列表发送到服务器,我正在使用TcpClient
类。
到目前为止,我正在通过单线程将数据同步发送到客户端。
byte[] bytes = Encoding.ASCII.GetBytes(message);
tcpClient.GetStream().Write(bytes, 0, bytes.Length);
//The client is already connected at the start
发送数据后,我将从列表中删除该条目。
这可以正常工作,但是发送数据的速度不够快,列表被迭代并一个接一个地发送,因此填充列表并占用更多内存。
我的问题是我可以使用同一tcpClient
对象从另一个线程并发写入,还是可以使用另一个tcpClient
对象与另一个线程中的同一服务器建立新连接?将数据发送到服务器的最有效(最快)方法是什么?
PS:我不想使用UDP
答案 0 :(得分:3)
正确;我认为这是一个有趣的话题。听起来好像您在多个线程之间共享一个套接字-完全有效的只要都非常小心。 TCP套接字是字节的逻辑流,因此您不能如此同时使用它,但是如果您的代码足够快,则可以非常有效地共享套接字,每个消息都为连续。
也许首先要看的是:您实际上是如何将数据写入套接字的?您的成帧/编码代码是什么样的?如果此代码很糟糕/效率很低:可能需要改进。例如,是否通过幼稚的byte[]
调用为每个string
间接创建了一个新的Encode
?是否有多个缓冲区?成帧时是否多次调用Send
?它如何处理数据包分段问题?等
首先尝试 -您可以避免一些缓冲区分配:
var enc = Encoding.ASCII;
byte[] bytes = ArrayPool<byte>.Shared.Rent(enc.GetMaxByteCount(message.Length));
// note: leased buffers can be oversized; and in general, GetMaxByteCount will
// also be oversized; so it is *very* important to track how many bytes you've used
int byteCount = enc.GetBytes(message, 0, message.Length, bytes, 0);
tcpClient.GetStream().Write(bytes, 0, byteCount);
ArrayPool<byte>.Shared.Return(bytes);
这使用租用缓冲区来避免每次创建byte[]
,这可以大大改善GC的影响。如果是我,我可能还会使用原始的Socket
而不是TcpClient
和Stream
的抽象,坦率地说,这些抽象不会给您带来太多好处。注意:如果您还有其他框架要做:将其包括在您租用的缓冲区的大小中,则在写入每段时使用适当的偏移量,并且只写一次一次-即准备整个缓冲区一次-避免多次调用Send
。
现在,听起来您有一个队列和一个专门的作家;也就是说,您的 app 代码将追加到队列中,而您的 writer 代码使事物出队,并将其写入套接字。尽管我会添加一些注释,但这是一种合理的实现方式:
List<T>
是一种实现队列的糟糕方式-从一开始就删除内容需要重新整理其他所有内容(这很昂贵);如果可能,请选择Queue<T>
,它会根据您的情况进行完美实现lock
即lock(queue) {queue.Enqueue(newItem);}
和SomeItem next; lock(queue) { next = queue.Count == 0 ? null : queue.Dequeue(); } if (next != null) {...write it...}
完成的。 此方法很简单,并且在避免数据包分段方面具有一些优势-编写器可以使用登台缓冲区,并且仅在缓冲某个阈值时或在以下情况下才实际写入套接字:队列为空-例如,但是当发生停顿时,它有可能创建大量积压。
但是!积压的事实表明某事没有跟上。这可能是网络(带宽),远程服务器(CPU)或本地出站网络硬件。如果这种情况只发生在可以自己解决的小问题上,那很好-(特别是如果某些出站消息很大时发生),但是:一个值得关注。
如果这种积压现象不断发生,那么坦率地说,您需要考虑到您对当前设计已经完全饱和了,因此您需要解除阻塞点之一:
注意:在涉及多个套接字的任何情况下,您都应注意不要发疯,并且 个专用工作线程太多;如果该数字超过了例如10个线程,您可能可能要考虑其他选择-可能涉及异步IO和/或管道(如下)。
为完整起见,另一种基本方法是从应用代码中编写 ;这种方法甚至更简单,并且避免了未发送工作的积压,但是:这意味着现在您的应用代码线程自己将在负载下进行备份。如果您的应用程序代码线程实际上是辅助线程,并且在sync / lock
上被阻止,那么这可能真的很糟糕。您不想不想饱和线程池,因为您可能会遇到以下情况:没有线程池线程可满足 unblock 所需的IO工作作家很活跃,可以让您陷入 real 问题。通常这不是您要用于高负载/高容量的方案,因为它很快就会出现问题-而且很难避免数据包分段,因为每个单独的消息都无法知道是否还会有更多消息进入
最近要考虑的另一种选择是“管道”;这是.NET中的一个新IO框架,旨在用于大量联网,尤其要注意异步IO,缓冲区重用以及良好实现的缓冲区/未完成日志机制,使使用简单的方法成为可能。 writer方法(在写入时同步),并且不转换为直接发送。-它表现为可以访问积压的异步作家,这使得避免数据包分段变得简单而有效。这是一个相当先进的领域,但它可能非常有效。给您带来问题的部分是:它设计用于整个异步使用,甚至是写操作-因此,如果您的应用程序代码当前处于同步状态,则实现起来可能会很痛苦。但是:这是要考虑的领域。我有很多关于此主题的博客文章,以及一系列OSS示例和真实库,它们利用了我可以向您指出的管道,但是:这不是“快速修复”-它是一个整个IO层的彻底改革。这也不是灵丹妙药-它只能消除由于本地IO处理成本而产生的开销。