12.3.8 基于线程的并发服务器

12.3.8 基于线程的并发服务器

图 12-14 展示了基于线程的并发 echo 服务器的代码。整体结构类似于基于进程的设计。主线程不断地等待连接请求,然后创建一个对等线程处理该请求。虽然代码看似简单,但是有几个普遍而且有些微妙的问题需要我们更仔细地看一看。第一个问题是当我们调用 pthread_create 时,如何将已连接描述符传递给对等线程。最明显的方法就是传递一个指向这个描述符的指针,就像下面这样

connfd = Accept(listenfd, (SA *) &clientaddr, &clientlen);
Pthread_create(&tid, NULL, thread, &connfd);

然后,我们让对等线程间接引用这个指针,并将它赋值给一个局部变量,如下所示

void *thread(void *vargp) {
    int connfd = *((int *)vargp);
    ...
}

code/conc/echoservert.c

#include "csapp.h"

void echo(int connfd);
void *thread(void *vargp);

int main(int argc, char **argv)
{
    int listenfd, *connfdp;
    socklen_t clientlen;
    struct sockaddr_storage clientaddr;
    pthread_t tid;

    if (argc != 2) {
        fprintf(stderr, "usage: %s <port>\n", argv[0]);
        exit(0);
    }
    listenfd = Open_listenfd(argv[1]);

    while (1) {
        clientlen=sizeof(struct sockaddr_storage);
        connfdp = Malloc(sizeof(int));
        *connfdp = Accept(listenfd, (SA *) &clientaddr, &clientlen);
        Pthread_create(&tid, NULL, thread, connfdp);
    }
}

/* Thread routine */
void *thread(void *vargp)
{
    int connfd = *((int *)vargp);
    Pthread_detach(pthread_self());
    Free(vargp);
    echo(connfd);
    Close(connfd);
    return NULL;
}

图 12-14 基于线程的并发 echo 服务器

然而,这样可能会出错,因为它在对等线程的赋值语句和主线程的 accept 语句间引入了竞争(race)。如果赋值语句在下一个 accept 之前完成,那么对等线程中的局部变量 connfd 就得到正确的描述符值。然而,如果赋值语句是在 accept 之后才完成的,那么对等线程中的局部变量 connfd 就得到下一次连接的描述符值。那么不幸的结果就是,现在两个线程在同一个描述符上执行输入和输出。为了避免这种潜在的致命竞争,我们必须将 accept 返回的每个已连接描述符分配到它自己的动态分配的内存块,如第 20~21 行所示。我们会在 12.7.4 节中回过来讨论竞争的问题。

另一个问题是在线程例程中避免内存泄漏。既然不显式地收回线程,就必须分离每个线程,使得在它终止时它的内存资源能够被收回(第 31 行)。更进一步,我们必须小心释放主线程分配的内存块(第 32 行)。

练习题 12.5 在图 12-5 中基于进程的服务器中,我们在两个位置小心地关闭了已连接描述符:父进程和子进程。然而,在图 12-14 中基于线程的服务器中,我们只在一个位置关闭了已连接描述符:对等线程。为什么?