我们假设我们正在构建一个旨在在具有四个核心的系统上运行的线程服务器。我能想到的两种线程管理方案是每个客户端连接一个线程和一个排队系统。
正如第一个系统的名称所暗示的那样,我们将为每个连接到我们服务器的客户端生成一个线程。假设一个线程始终专用于我们程序的主要执行线程,我们将能够同时处理多达三个客户端,并且对于任何更多的同时客户端,我们将不得不依赖于操作系统的抢占式多任务功能可以在它们之间切换(或者在绿色线程的情况下是VM)。
对于我们的第二种方法,我们将制作两个线程安全的队列。一个用于传入消息,一个用于传出消息。换句话说,请求和回复。这意味着我们可能有一个线程接受传入连接并将其请求放入传入队列。一个或两个线程将处理传入请求的处理,解析相应的回复,并将这些回复放在传出队列上。最后,我们有一个线程只是从该队列中回复并将它们发送回客户端。
这些方法的优点和缺点是什么?请注意,我没有提到这是什么类型的服务器。我假设哪一个具有更好的性能配置文件取决于服务器是处理短连接,如Web服务器和POP3服务器,还是更长的连接,如WebSocket服务器,游戏服务器和消息应用服务器。
除了这两个之外还有其他线程管理策略吗?
答案 0 :(得分:2)
我相信我曾经同时做过这两个组织。
方法1
我们就在同一页面上,第一个让主线程执行listen
。然后,在一个循环中,它accept
。然后它将返回值传递给pthread_create
,并且客户端线程的循环在循环处理recv/send
时处理远程客户端想要的所有命令。完成后,它会清理并终止。
有关此示例,请参阅我最近的回答:multi-threaded file transfer with socket
这具有主线程和客户端线程简单且独立的优点。没有线程等待另一个线程正在做的事情。没有线程在等待它不需要的任何东西。因此,客户端线程[复数]都可以以最大线速度运行。此外,如果客户端线程在recv
或send
上被阻止,而另一个线程可以继续,则会。这是自我平衡。
所有线程循环都很简单:wait for input, process, send output, repeat
。即使是主线程也很简单:sock = accept, pthread_create(sock), repeat
另一件事。客户端线程与其远程客户端之间的交互可以是他们同意的任何。任何协议或任何类型的数据传输。
方法2
这有点类似于N工人模型,其中N是固定的。
因为accept
[通常]是阻塞的,所以我们需要一个类似于方法1的主线程。除了,它不需要启动一个新线程,它需要malloc一个控制结构[或其他一些管理方案]并将套接字放入其中。然后将其放在客户端连接列表中,然后循环回accept
除了N个工作线程之外,你是对的。至少两个控制线程,一个用于执行select/poll
,recv
,enqueue request
,一个用于执行wait for result
,select/poll
,{ {1}}。
需要两个线程来防止其中一个线程必须等待两个不同的东西:各种套接字[作为一个组]和来自各个工作线程的请求/结果队列。使用单个控制线程,所有操作都必须非 - 阻塞,并且线程会像疯了一样旋转。
以下是线程外观的[非常]简化版本:
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);
}
控制线程不尝试选择空闲[或未充分利用]工作线程并入队到特定于线程的队列[这是另一个需要考虑的选项]。
单个请求队列可能是最有效的。但是,也许并非所有工作线程都是平等的。有些可能最终会出现具有特殊加速度H / W的CPU核心[或集群节点],因此某些请求可能将发送到特定线程。
并且,如果这样做,线程可以做"偷窃"?也就是说,一个线程完成了它的所有工作,并注意到另一个线程在其队列中有一个请求[兼容]但尚未启动。该线程使请求出列并开始处理它。
这是这种方法的一大缺点。请求/结果块[大多]是固定大小的。我已经完成了一个实现,其中控件可以有一个字段用于" side / extra"有效负载指针,可以是任意大小。
但是,如果进行大量的传输文件传输,无论是上传还是下载,尝试通过请求块传递这个零碎都不是一个好主意。
在下载的情况下,工作线程可以临时篡改套接字并recv
文件数据,然后将结果排入控制线程。
但是,对于上传案例,如果工作人员试图在紧密循环中进行上传,则会与send
控制线程发生冲突。工作人员必须[以某种方式]警告控制线程不在其轮询掩码中包含套接字。
这开始变得复杂。
并且,所有这个请求/结果块都有入队/出队的开销。
此外,两个控制线是一个热点"。系统的整个吞吐量取决于它们。
而且,套接字之间存在交互。在简单的情况下,recv
线程可以在一个套接字上启动,但是其他希望发送请求的客户端会延迟到recv
完成。这是一个瓶颈。
这意味着所有recv
系统调用都必须是非阻塞的[异步]。控制线程必须管理这些异步请求(即启动一个并等待异步完成通知,仅然后将请求排入请求队列)。
这开始变得复杂。
想要这样做的主要好处是拥有大量的并发客户端(例如50,000),但要将线程数保持为理智值(例如100)。
此方法的另一个优点是可以分配优先级并使用多个优先级队列
比较和杂交
同时,方法1执行方法2所做的一切,但是更简单,更健壮[并且,我怀疑,更高的吞吐量方式]。
创建方法1客户端线程后,它可能会拆分工作并创建多个子线程。然后它可以像方法2的控制线程一样。实际上,它可能会像方法2一样从固定的N池中利用这些线程。
这将弥补方法1的弱点,其中线程将进行大量计算。由于大量线程都在进行计算,系统会被淹没。排队方法有助于缓解这种情况。客户端线程仍然是创建/活动的,但它在结果队列中休眠。
所以,我们只是稍微混淆了水域。
任何一种方法都可以是"前面的"方法,并在下面有其他元素。
给定的客户端线程[方法1]或工作者线程[方法2]可以通过打开[还]到"后台办公室的另一个连接来扩展其工作。计算集群。可以使用任一方法管理集群。
因此,方法1更简单,更容易实现,并且可以轻松容纳大多数工作组合。对于繁重的计算服务器来说,方法2可能更好地限制对有限资源的请求。但是,必须注意方法2以避免瓶颈。
答案 1 :(得分:0)
我不认为你的“第二种方法”是经过深思熟虑的,所以我只是看看能不能告诉你如何才能找到最有用的东西。
规则1)如果您的所有核心都忙着做有用的工作,那么您的吞吐量会最大化。尽量让你的核心忙于做有用的工作。
这些东西可以阻止你让核心忙于做有用的工作:
你正在忙着创建线程。如果任务是短暂的,那么使用线程池,这样你就不会花费所有时间来启动和杀死线程。
你让他们忙着切换上下文。现代操作系统非常擅长多线程,但如果你必须每秒切换10000次作业,那么这种开销就会增加。如果这对您来说是一个问题,您将不得不考虑和事件驱动的架构或其他更有效的显式调度。
您的作业阻塞或等待很长时间,并且您没有足够的资源来运行足够的线程线程来保持核心繁忙。当您使用持久连接提供协议时,这可能会成为一个问题,这些连接在大多数情况下都无所事事,例如websocket chat。您不希望通过将其绑定到单个客户端来保持整个线程无所事事。你需要围绕这个来构建。
除了CPU之外,你所有的工作都需要一些其他的资源,你就会遇到瓶颈 - 这是另一天的讨论。
所有这些......对于大多数请求/响应类型的协议,将每个请求或连接传递给在请求期间为其分配线程的线程池,在大多数情况下很容易实现和执行。
规则2)鉴于最大化吞吐量(所有核心都非常繁忙),以先到先得的方式完成工作可以最大限度地减少延迟并最大限度地提高响应速度。
这是事实,但在大多数服务器中根本没有考虑。当您的服务器忙碌时,您可能会遇到麻烦,即使是很短的时间,工作也必须停止执行大量的阻止操作。
问题在于,没有什么可以告诉操作系统线程调度程序首先进入哪个线程的作业。每当你的线程阻塞然后准备就绪时,它就会与所有其他线程在相同的条件下进行调度。如果服务器正忙,这意味着处理请求所需的时间与阻止的次数大致成正比。这通常没有用。
如果您必须在处理作业的过程中阻塞很多,并且希望最小化每个请求的总体延迟,则必须执行自己的计划,以便跟踪首先启动的作业。例如,在事件驱动的体系结构中,您可以优先处理先前开始的作业的事件。在流水线架构中,您可以优先考虑管道的后续阶段。
请记住这两条规则,设计服务器以保持核心忙于有用的工作,并先做好第一件事。然后你就可以拥有一台快速响应的服务器。