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

Multithreading 每个客户端一个线程和线程服务器的队列线程模型之间的相对优点?,multithreading,Multithreading,假设我们正在构建一个线程服务器,该服务器打算在一个具有四个内核的系统上运行。我能想到的两种线程管理方案是每个客户端连接一个线程和一个排队系统 正如第一个系统的名称所暗示的,我们将为每个连接到服务器的客户机生成一个线程。假设一个线程总是专用于我们程序的主执行线程,我们将能够同时处理多达三个客户端,并且对于任何更多的并发客户端,我们将不得不依赖操作系统的抢占式多任务功能在它们之间切换(或在绿色线程的情况下使用VM) 对于第二种方法,我们将创建两个线程安全队列。一个用于传入消息,一个用于传出消息。换句

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

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

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

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


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

我相信我曾经在这两种组织中使用过


方法1

我们在同一个页面上,第一个让主线程执行一个
listen
。然后,在循环中,它接受
。然后它将返回值传递给
pthread\u create
,客户端线程的循环执行
recv/send
循环内处理远程客户端需要的所有命令。完成后,它将清理并终止

有关这方面的示例,请参见我最近的回答:

它的优点是主线程和客户端线程都是直接和独立的。没有线程等待其他线程正在执行的任何操作。没有线程在等待它不需要的任何东西。因此,客户端线程[复数]都可以以最大线速度运行。另外,如果一个客户端线程在
recv
send
上被阻塞,而另一个线程可以去,它将去。这是自我平衡

所有线程循环都很简单:
等待输入、处理、发送输出、重复。甚至主线程也很简单:
sock=accept,pthread\u create(sock),repeat

还有一件事。客户机线程与其远程客户机之间的交互可以是双方同意的任何内容。任何协议或任何类型的数据传输


方法2

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

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

除了N个辅助线程之外,您是正确的。至少有两个控制线程,一个执行
选择/轮询
接收
排队请求
,一个执行
等待结果
选择/轮询
发送

需要两个线程来防止其中一个线程必须等待两件不同的事情:不同的套接字[作为一个组]和来自不同工作线程的请求/结果队列。对于单个控制线程,所有操作都必须是非阻塞的,线程会疯狂地旋转

以下是线程外观的[极其]简化版本:

// 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
控制线程冲突。工作者必须[以某种方式]提醒控制线程不要将套接字包含在其轮询掩码中

这开始变得复杂起来

而且,所有这些请求/结果块的入/出队列都有开销

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

而且,它们之间存在相互作用