我应该为多套接字客户端使用多个线程吗?

Should I use multiple threads for a multi socket client?

本文关键字:线程 套接字 客户端 我应该      更新时间:2023-10-16

我知道在大多数情况下,在Qt网络中使用线程是矫枉过正和不必要的,特别是如果你以正确的方式使用readyRead()信号。但是,我的"客户端"应用程序将一次打开多个套接字(大约 5 个)。有可能同时有数据传入所有套接字。我真的不会对传入的数据进行任何密集的处理。只需将其读入,然后发送信号以使用新接收的数据更新 GUI。您认为单线程应用程序应该能够处理传入的所有数据吗?

我知道我没有向你展示任何代码,而且我的描述非常模糊,这很可能取决于它一旦实现后的性能,但从一般设计的角度来看和你们的专业知识,你有什么意见?

除非您接收的是真正的高带宽流(例如每秒兆字节而不是每秒千字节),否则单线程设计应该就足够了。 请记住,操作系统的网络堆栈始终在"后台"运行,接收 TCP 数据包并将接收的数据存储在固定大小的内核内存缓冲区中。 这与程序的执行并行发生,因此在大多数情况下,程序是单线程的并且忙于处理GUI更新(或其他套接字)这一事实不会妨碍计算机接收TCP数据包。

单线程设计会导致TCP流量变慢的情况是,如果您的程序(通过Qt)没有足够快地调用recv(),以至于内核的套接字TCP接收缓冲区完全被数据填满。 此时,内核别无选择,只能开始丢弃该套接字的传入 TCP 数据包,这将导致服务器必须重新发送这些 TCP 数据包,并且会导致套接字的 TCP 接收速率减慢,至少是暂时的。 但是,通过确保缓冲区永远不会(或至少很少)变满,可以避免这个问题。

这样做的明显方法是确保程序尽快读取所有传入数据 - QTCPSocket默认这样做。 你唯一需要做的就是确保你的GUI更新不会花费过多的时间——Qt的小部件更新例程相当高效,所以它们不应该,除非你有一个非常复杂的GUI或低效的自定义paintEvent()例程等。

如果这还不够,您可以做的下一件事(如有必要)是告诉操作系统的 TCP 堆栈增加其内核内 TCP 接收缓冲区的大小,例如通过执行以下操作:

 int fd = myQTCPSocketObject.descriptor();
 int newBufSizeBytes = 128*1024;   // request 128kB kernel recv-buffer for this socket
 if (setsockopt(fd, SOL_SOCKET, SO_RCVBUF, &newBufSizeBytes, sizeof(newBufSizeBytes)) != 0) perror("setsockopt");

这样做可以让您的(单)线程有更多时间做出反应,以免传入数据包因缺乏内核缓冲区空间而开始丢弃。

如果在尝试了所有这些之后,您仍然无法获得所需的网络性能,那么您可以尝试使用多线程。 我怀疑它会变成这样,但如果它确实如此,它不需要对你的程序设计产生太大影响;您只需编写一个包装类(称为 SocketThread 或其他东西)来保存您的 QTCPSocket 对象并运行一个内部线程来处理从套接字读取,并在线程从套接字读取数据时发出 bytesReceived(QByteArray) 信号。 代码的其余部分将大致保持不变;只需修改它以保存 SocketThread 对象而不是 QTCPSocket,并将 SocketThread 的 bytesReceived(QByteArray) 信号连接到相应的插槽(当然,通过 QueuedConnection,为了线程安全)并使用它而不是直接响应 readReady()。

在没有线程的情况下实现它,使用线程考虑的设计(*),测量数据经历的延迟,确定它是否在可接受的范围内。然后决定是否需要使用线程来更快地捕获它。

从您的描述来看,关键瓶颈将是"数据就绪"信号的GUI接收,渲染它。如果您使用发送大量这些信号的方法,您的 GUI 将执行更多的重新渲染。

如果使用单线程方法,则可以封送网络读取并获取所有更新,然后直接刷新 GUI。正如您所描述的,这听起来像是争用程度最小。

(* 尽量避免使用需要完全重写的构造,如果你去线程化,但不要花太多精力让它成为线程证明,它实际上需要线程来提高效率,例如不要用互斥调用包装所有内容)

我对Qt了解不多,但这可能是使用select()通过单个线程多路复用多个套接字访问的典型场景。

如果用于选择的线程主要用于处理来自/到套接字的数据,您将非常快(因为您将拥有较少的上下文切换)。因此,如果您不传输大量数据,则可能会更快地使用单线程解决方案。

话虽如此,我会选择最适合您需求的解决方案,您可以在相当长的时间内实施。实现select(异步)可能相当麻烦,可能不需要矫枉过正。

这是一种类似 C 的方法,但我希望我无论如何都能提供帮助。