我相信我曾经做过这两个组织。
方法一
就像我们在同一页面上一样,第一个让主线程执行listen。然后,在一个循环中,它执行accept。然后它将返回值传递给pthread_create,并且客户端线程的循环执行recv/send in loop 处理远程客户端想要的所有命令。完成后,它会清理并终止。
有关此示例,请参阅我最近的回答:multi-threaded file transfer with socket
这具有主线程和客户端线程直接且独立的优点。没有线程等待另一个线程正在做的任何事情。没有线程正在等待它不需要的任何东西。因此,客户端线程 [复数] 都可以以最大线速运行。此外,如果一个客户端线程在recv 或send 上被阻塞,而另一个线程可以去,它就会去。它是自我平衡的。
所有线程循环都很简单:wait for input, process, send output, repeat。连主线程也很简单:sock = accept, pthread_create(sock), repeat
另一件事。客户端线程与其远程客户端之间的交互可以是任何他们同意的。任何协议或任何类型的数据传输。
方法二
这有点类似于 N 工人模型,其中 N 是固定的。
因为accept [通常] 是阻塞的,我们需要一个类似于方法 1 的主线程。除了启动一个新线程,它需要 malloc 一个控制结构 [或其他一些mgmt scheme] 并将套接字放入其中。然后它将它放在客户端连接列表中,然后循环回accept
除了N个工作线程,你是对的。至少两个控制线程,一个做select/poll、recv、enqueue request,一个做wait for result、select/poll、send。
需要两个线程来防止其中一个线程不得不等待两个不同的事情:各种套接字[作为一个组]和来自各种工作线程的请求/结果队列。对于单个控制线程,所有操作都必须是非阻塞的,并且线程会像疯了一样旋转。
这是线程外观的[极其]简化版本:
// control thread for recv:
while (1) {
// (1) do blocking poll on all client connection sockets for read
poll(...)
// (2) for all pending sockets do a recv for a request block and enqueue
// it on the request queue
for (all in read_mask) {
request_buf = dequeue(control_free_list)
recv(request_buf);
enqueue(request_list,request_buf);
}
}
// control thread for recv:
while (1) {
// (1) do blocking wait on result queue
// (2) peek at all result queue elements and create aggregate write mask
// for poll from the socket numbers
// (3) do blocking poll on all client connection sockets for write
poll(...)
// (4) for all pending sockets that can be written to
for (all in write_mask) {
// find and dequeue first result buffer from result queue that
// matches the given client
result_buf = dequeue(result_list,client_id);
send(request_buf);
enqueue(control_free_list,request_buf);
}
}
// worker thread:
while (1) {
// (1) do blocking wait on request queue
request_buf = dequeue(request_list);
// (2) process request ...
// (3) do blocking poll on all client connection sockets for write
enqueue(result_list,request_buf);
}
现在,有几点需要注意。只有 一个 请求队列用于所有工作线程。 recv 控制线程没有尝试选择空闲[或未充分利用的]工作线程并加入线程特定队列[这是另一个需要考虑的选项]。
单个请求队列可能是最有效的。但是,也许并非所有工作线程都是平等的。有些可能最终会出现在具有特殊加速硬件的 CPU 内核 [或集群节点] 上,因此有些请求可能必须发送到特定线程。
而且,如果这样做了,线程可以做“工作窃取”吗?也就是说,一个线程完成了它的所有工作,并注意到另一个线程在它的队列[兼容]中有一个请求,但还没有启动。线程将请求出列并开始处理它。
这种方法有一个很大的缺点。请求/结果块 [大部分] 是固定大小的。我已经完成了一个实现,其中控件可以有一个用于“side/extra”有效负载指针的字段,该指针可以是任意大小。
但是,如果进行大型传输文件传输,无论是上传还是下载,尝试通过请求块传递这些零碎的东西并不是一个好主意。
在下载的情况下,工作线程可以在将结果排入控制线程之前临时篡夺套接字和send文件数据。
但是,对于上传的情况,如果worker试图在一个紧密的循环中进行上传,它将与recv控制线程发生冲突。工作人员必须 [以某种方式] 提醒控制线程不在其轮询掩码中包含套接字。
这开始变得复杂了。
而且,所有这些请求/结果块入队/出队都有开销。
另外,两个控制线程是一个“热点”。系统的整个吞吐量取决于它们。
而且,套接字之间存在交互。在简单的情况下,recv 线程可以启动一对一套接字,但其他希望发送请求的客户端会延迟到 recv 完成。这是一个瓶颈。
这意味着所有recv 系统调用必须是非阻塞的[异步]。控制线程必须管理这些异步请求(即启动一个并等待异步完成通知,仅然后将请求排入请求队列)。
这开始变得复杂了。
这样做的主要好处是拥有大量并发客户端(例如 50,000),但将线程数保持在一个合理的值(例如 100)。
此方法的另一个优点是可以分配优先级并使用多个优先级队列
比较和混合
同时,方法 1 完成了方法 2 所做的所有事情,但以一种更简单、更健壮的方式 [而且,我怀疑是更高吞吐量的方式]。
在创建方法 1 客户端线程后,它可能会拆分工作并创建多个子线程。然后它可以像方法 2 的控制线程一样工作。事实上,它可以像方法 2 一样从固定的 N 个池中提取这些线程。
这将弥补方法 1 的弱点,即线程将进行大量计算。如果有大量线程都在做计算,系统就会被淹没。排队方法有助于缓解这种情况。客户端线程仍处于创建/活动状态,但它正在结果队列中休眠。
所以,我们只是把水弄混了一点。
任何一种方法都可以是“正面”方法,并且在下面有另一种方法的元素。
给定的客户端线程 [方法 1] 或工作线程 [方法 2] 可以通过打开 [yet] 到“后台”计算集群的另一个连接来分包其工作。可以使用任一方法管理集群。
因此,方法 1 更简单、更易于实施,并且可以轻松适应大多数工作组合。方法 2 可能更适合重型计算服务器来限制对有限资源的请求。但是,必须小心方法 2,以避免出现瓶颈。