我相信我曾经做过这两个组织。
Method 1
就这样我们在同一页面上,第一个让主线程执行listen
。然后,在一个循环中,它确实accept
。然后它将返回值传递给pthread_create
并且客户端线程的循环确实recv/send
循环处理远程客户端想要的所有命令。完成后,它会清理并终止。
有关这方面的示例,请参阅我最近的回答:使用socket进行多线程文件传输 https://stackoverflow.com/questions/37497556/multi-threaded-file-transfer-with-socket/37506686#37506686
这样做的优点是主线程和客户端线程简单且独立。没有线程会等待另一个线程正在执行的任何操作。没有线程会等待任何不必要的事情。因此,客户端线程[多个]都可以以最大线速度运行。另外,如果客户端线程被阻塞recv
or send
,另一个线程可以走了,它会的。这是自我平衡。
所有线程循环都很简单:wait for input, process, send output, repeat
。甚至主线程也很简单:sock = accept, pthread_create(sock), repeat
另一件事。客户端线程与其远程客户端之间的交互可以是anything他们同意。任何协议或任何类型的数据传输。
Method 2
这有点类似于 N 工作模型,其中 N 是固定的。
因为accept
[通常]是阻塞的,我们需要一个与方法 1 类似的主线程。除了,它不需要启动一个新线程,而是需要 malloc 一个控制结构 [或其他一些管理方案] 并将套接字放入那。然后它将其放入客户端连接列表中,然后循环回accept
除了N个工作线程之外,你是对的。至少two控制线程,一件事情select/poll
, recv
, enqueue request
和一件要做的事wait for result
, select/poll
, send
.
需要两个线程来防止其中一个线程必须等待两个不同的事物:各种套接字[作为一个组]以及来自各个工作线程的请求/结果队列。对于单个控制线程,所有操作都必须是non- 阻塞,线程会疯狂旋转。
这是线程的[极其]简化版本:
// 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);
}
现在,有几点需要注意。仅有的one请求队列用于所有工作线程。这recv
控制线程做了not尝试选择一个空闲[或未充分利用]的工作线程并排队到特定于线程的队列[这是要考虑的另一个选项]。
单个请求队列可能是最有效的。但是,也许并非所有工作线程都是平等创建的。有些可能最终出现在具有特殊加速硬件的 CPU 核心 [或集群节点] 上,因此某些请求可能会have发送到特定线程。
而且,如果这样做了,线程可以“窃取工作”吗?也就是说,一个线程完成其所有工作,并注意到另一个线程在其队列中有一个请求[兼容]但尚未启动。线程使请求出队并开始处理它。
这种方法有一个很大的缺点。请求/结果块[大部分]具有固定大小。我已经完成了一个实现,其中控件可以有一个用于“侧面/额外”有效负载指针的字段,该指针可以是任意大小。
但是,如果进行大型传输文件传输(无论是上传还是下载),尝试通过请求块传递这些零碎的文件并不是一个好主意。
在下载情况下,工作线程可能会暂时占用套接字并send
在将结果排队到控制线程之前先读取文件数据。
但是,对于上传情况,如果工作人员尝试在紧密循环中进行上传,则会与recv
控制线程。工作人员必须[以某种方式]提醒控制线程not将套接字包含在其轮询掩码中。
这开始变得复杂。
而且,所有这些请求/结果块入队/出队都会产生开销。
此外,两个控制线程也是一个“热点”。系统的整个吞吐量取决于它们。
并且,套接字之间存在交互。在简单的情况下,recv
线程可以在一个套接字上一对一地启动,但其他希望发送请求的客户端会被延迟,直到recv
完成。这是一个瓶颈。
这意味着所有recv
系统调用必须是非阻塞的[异步]。控制线程必须管理这些异步请求(即启动一个请求并等待异步完成通知,以及only然后将请求放入请求队列)。
这开始变得复杂。
这样做的主要好处是拥有大量并发客户端(例如 50,000 个),但将线程数量保持在合理的值(例如 100)。
该方法的另一个优点是可以分配优先级并使用多个优先级队列
比较和混合
同时,方法 1 执行方法 2 执行的所有操作,但以更简单、更稳健的方式 [并且我怀疑,吞吐量更高]。
创建方法 1 客户端线程后,它可能会拆分工作并创建多个子线程。然后,它可以像方法 2 的控制线程一样运行。事实上,它可以像方法 2 一样从固定的 N 池中利用这些线程。
这将弥补方法 1 的弱点,即线程将执行大量计算。如果有大量线程都在进行计算,系统就会陷入困境。排队方法有助于缓解这种情况。客户端线程仍然被创建/活动,但它在结果队列上休眠。
所以,我们只是把水搅得更浑了。
任何一种方法都可以是“正面”方法,并且在下面具有另一种方法的元素。
给定的客户端线程[方法 1] 或工作线程[方法 2] 可以通过打开与“后台”计算集群的另一个连接来分担其工作。可以使用任一方法来管理集群。
因此,方法 1 更简单、更容易实施,并且可以轻松适应大多数作业组合。对于重型计算服务器来说,方法 2 可能更好,可以限制对有限资源的请求。但是,必须谨慎使用方法 2,以避免出现瓶颈。