多线程 – 每个客户端一个线程和线程服务器的排队线程模型之间的相对优点?

假设我们正在构建一个旨在在具有四个核心的系统上运行的线程服务器.我能想到的两个线程管理方案是每个客户端连接一个线程和一个排队系统.

正如第一个系统的名称所暗示的那样,我们将为每个连接到我们服务器的客户端生成一个线程.假设一个线程始终专用于我们程序的主要执行线程,我们将能够同时处理多达三个客户端,并且对于任何更多的并发客户端,我们将不得不依赖于操作系统的抢占式多任务处理功能来切换它们(或绿色线程中的VM).

对于我们的第二种方法,我们将创建两个线程安全的队列.一个用于传入消息,一个用于传出消息.换句话说,请求和回复.这意味着我们可能有一个线程接受传入连接并将其请求放入传入队列.一个或两个线程将处理传入请求的处理,解析相应的回复,并将这些回复放在传出队列上.最后,我们将有一个线程只是从该队列中回复并将它们发送回客户端.

这些方法的优点和缺点是什么?请注意,我没有提到这是什么类型的服务器.我假设哪个具有更好的性能配置文件取决于服务器是处理短连接,如Web服务器和POP3服务器,还是更长的连接,如WebSocket服务器,游戏服务器和消息传递应用服务器.

除了这两个之外还有其他线程管理策略吗?

最佳答案 我相信我曾经两次都做过这两个组织.

方法1

就这样我们在同一页面上,第一个主线程做了一个监听.然后,在一个循环中,它确实接受.然后它将返回值传递给pthread_create,并且客户端线程的循环确实在循环处理recv / send处理远程客户端想要的所有命令.完成后,它会清理并终止.

有关此示例,请参阅我最近的回答:multi-threaded file transfer with socket

这具有主线程和客户端线程简单且独立的优点.没有线程等待另一个线程正在做的事情.没有线程在等待它不需要的任何东西.因此,客户端线程[复数]都可以以最大线速度运行.此外,如果客户端线程在recv或send上被阻止,而另一个线程可以,则会.这是自我平衡.

所有线程循环都很简单:等待输入,处理,发送输出,重复.即使主线程很简单:sock = accept,pthread_create(sock),重复

另一件事.客户端线程与其远程客户端之间的交互可以是他们同意的任何内容.任何协议或任何类型的数据传输.

方法2

这有点类似于N工人模型,其中N是固定的.

因为accept [通常]是阻塞的,所以我们需要一个类似于方法1的主线程.除了,它不需要启动一个新的线程,它需要malloc一个控制结构[或其他一些管理方案]并放入那个插座.然后将它放在客户端连接列表上,然后循环回到accept

除了N个工作线程之外,你是对的.至少有两个控制线程,一个用于执行select / poll,recv,enqueue请求,另一个用于等待结果,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控制线程没有尝试选择空闲[或未充分利用]工作线程并入队到特定于线程的队列[这是另一个需要考虑的选项].

单个请求队列可能是最有效的.但是,也许并非所有工作线程都是平等的.有些可能最终会出现具有特殊加速H / W的CPU核心[或集群节点],因此可能必须将某些请求发送到特定线程.

而且,如果这样做,一个线程可以“偷窃”吗?也就是说,一个线程完成了它的所有工作,并注意到另一个线程在其队列中有一个请求[兼容]但尚未启动.线程将请求出列并开始处理它.

这是这种方法的一大缺点.请求/结果块[大多]是固定大小的.我已经完成了一个实现,其中控件可以有一个“side / extra”有效负载指针的字段,可以是任意大小.

但是,如果进行大量的传输文件传输,无论是上传还是下载,尝试通过请求块传递这个零碎都不是一个好主意.

在下载的情况下,工作线程可以临时篡改套接字并在将结果排入控制线程之前发送文件数据.

但是,对于上传案例,如果工作人员试图在紧密循环中进行上传,则会与recv控制线程发生冲突.工作人员必须[以某种方式]警告控制线程不要在其poll掩码中包含套接字.

这开始变得复杂.

并且,所有这个请求/结果块入队/出队都有开销.

此外,两个控制线程是“热点”.系统的整个吞吐量取决于它们.

并且,套接字之间存在交互.在简单的情况下,recv线程可以在一个套接字上启动,但是其他希望发送请求的客户端会延迟到recv完成.这是一个瓶颈.

这意味着所有recv系统调用都必须是非阻塞的[异步].控制线程必须管理这些异步请求(即,启动一个并等待异步完成通知,然后才将请求排入请求队列).

这开始变得复杂.

想要这样做的主要好处是拥有大量的同时客户端(例如50,000),但是将线程数保持为合理的值(例如100).

该方法的另一个优点是可以分配优先级并使用多个优先级队列

比较和杂交

同时,方法1执行方法2所做的一切,但是更简单,更健壮[并且,我怀疑,更高的吞吐量方式].

在创建方法1客户端线程之后,它可能会拆分工作并创建多个子线程.然后,它可以像方法2的控制线程一样.事实上,它可能像方法2一样从固定的N池中利用这些线程.

这将弥补方法1的弱点,其中线程将进行大量计算.由于大量线程都在进行计算,系统会被淹没.排队方法有助于缓解这种情况.客户端线程仍然是创建/活动的,但它正在结果队列中休眠.

所以,我们只是把水更加混乱了.

任何一种方法都可以是“前置”方法,并且其他方法的元素可以在下面.

给定的客户端线程[方法1]或工作线程[方法2]可以通过打开[又]到“后台”计算集群的另一个连接来扩展其工作.可以使用任一方法管理群集.

因此,方法1更简单,更容易实现,并且可以轻松容纳大多数工作组合.对于繁重的计算服务器来说,方法2可能更好地限制对有限资源的请求.但是,必须注意方法2以避免瓶颈.

点赞