如何从多个线程写入TcpListener?

时间:2019-02-15 08:25:34

标签: c# tcpclient

比方说,我有一个静态列表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

1 个答案:

答案 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而不是TcpClientStream的抽象,坦率地说,这些抽象不会给您带来太多好处。注意:如果您还有其他框架要做:将其包括在您租用的缓冲区的大小中,则在写入每段时使用适当的偏移量,并且只写一次一次-即准备整个缓冲区一次-避免多次调用Send


现在,听起来您有一个队列和一个专门的作家;也就是说,您的 app 代码将追加到队列中,而您的 writer 代码使事物出队,并将其写入套接字。尽管我会添加一些注释,但这是一种合理的实现方式:

  • List<T>是一种实现队列的糟糕方式-从一开始就删除内容需要重新整理其他所有内容(这很昂贵);如果可能,请选择Queue<T>,它会根据您的情况进行完美实现
  • 这将需要同步,这意味着您需要确保一次只有一个线程更改队列-通常是通过简单的locklock(queue) {queue.Enqueue(newItem);}SomeItem next; lock(queue) { next = queue.Count == 0 ? null : queue.Dequeue(); } if (next != null) {...write it...}完成的。

此方法很简单,并且在避免数据包分段方面具有一些优势-编写器可以使用登台缓冲区,并且仅在缓冲某个阈值时或在以下情况下才实际写入套接字:队列为空-例如,但是当发生停顿时,它有可能创建大量积压。

但是!积压的事实表明某事没有跟上。这可能是网络(带宽),远程服务器(CPU)或本地出站网络硬件。如果这种情况只发生在可以自己解决的小问题上,那很好-(特别是如果某些出站消息很大时发生),但是:一个值得关注。

如果这种积压现象不断发生,那么坦率地说,您需要考虑到您对当前设计已经完全饱和了,因此您需要解除阻塞点之一:

  • 确保您的编码代码高效是第零步
  • 您可以将 encode 步骤移至应用代码中,即,在获取锁之前,在之前准备一个框架,对消息进行编码,然后只排队一个完全准备好的框架;这意味着writer线程除了出队,写入和回收之外,不需要执行任何其他操作,但这会使缓冲区管理更加复杂(显然,在缓冲区被完全处理之前,您无法回收缓冲区)
  • 如果您尚未采取措施来减少数据包碎片,则可能会大有帮助。
  • 否则,您可能需要(在调查堵塞之后):
    • 更好的局域网硬件(NIC)或物理机器硬件(CPU等)
    • 多个套接字(和队列/工人)之间进行循环,以分配负载
    • 可能是多个服务器进程,每个服务器都有一个端口,因此您的多个套接字正在与不同的进程通信
    • 更好的服务器
    • 多个服务器

注意:在涉及多个套接字的任何情况下,您都应注意不要发疯,并且 个专用工作线程太多;如果该数字超过了例如10个线程,您可能可能要考虑其他选择-可能涉及异步IO和/或管道(如下)。


为完整起见,另一种基本方法是从应用代码中编写 ;这种方法甚至更简单,并且避免了未发送工作的积压,但是:这意味着现在您的应用代码线程自己将在负载下进行备份。如果您的应用程序代码线程实际上是辅助线程,并且在sync / lock上被阻止,那么这可能真的很糟糕。您不想不想饱和线程池,因为您可能会遇到以下情况:没有线程池线程可满足 unblock 所需的IO工作作家很活跃,可以让您陷入 real 问题。通常这不是您要用于高负载/高容量的方案,因为它很快就会出现问题-而且很难避免数据包分段,因为每个单独的消息都无法知道是否还会有更多消息进入


最近要考虑的另一种选择是“管道”;这是.NET中的一个新IO框架,旨在用于大量联网,尤其要注意异步IO,缓冲区重用以及良好实现的缓冲区/未完成日志机制,使使用简单的方法成为可能。 writer方法(在写入时同步),并且不转换为直接发送。-它表现为可以访问积压的异步作家,这使得避免数据包分段变得简单而有效。这是一个相当先进的领域,但它可能非常有效。给您带来问题的部分是:它设计用于整个异步使用,甚至是写操作-因此,如果您的应用程序代码当前处于同步状态,则实现起来可能会很痛苦。但是:这是要考虑的领域。我有很多关于此主题的博客文章,以及一系列OSS示例和真实库,它们利用了我可以向您指出的管道,但是:这不是“快速修复”-它是一个整个IO层的彻底改革。这也不是灵丹妙药-它只能消除由于本地IO处理成本而产生的开销。