网络编程:阻塞I/O和线程模型

线程

进程模型在处理用户请求的过程中,进程切换上下文的代价比较高,而,一种轻量级的模型可以处理多用户连接请求,那就是线程模型
线程(thread)是运行在进程中的一个“逻辑流”,现代操作系统都允许在单进程中运行多个线程。线程由操作系统内核管理。每个线程都有自己的上下文(context),包括一个可以唯一标识线程的ID(thread ID,或叫tid)、栈、程序计数器、寄存器等。在同一个进程中,所有的线程共享该进程的整个虚拟地址空间,包括代码、数据、堆、共享库等。
每个进程一开始就会产生一个线程,一般称为主线程,主线程可以再产生子线程,这样的主线程-子线程对可以叫做一个对等线程。

有多进程处理并发,为什么还需要多线程处理并发?

简单来说,就是在同一个进程下,线程上下文切换的开销要比进程小的多

如何理解上下文呢?

我们的代码被CPU执行的时候,是需要一些数据支持的,比如程序计数器告诉CPU代码执行到哪里了,寄存器里存了当前计算的一些中间值,内存里放置了一些当前用到的变量等,从一个计算场景,切换到另一个计算场景,程序计数器、寄存器等这些值重新载入新场景的值,就是线程的上下文切换。

主要线程函数

创建线程

pthread_create函数用来创建一个线程。

int pthread_create(pthread_t *tid, const pthread_attr_t *attr,
           void *(*func)(void *), void *arg);

返回:若成功则为0,若出错则为正的Exxx值

每个线程都有一个线程ID(tid)唯一标识,其数据类型为pthread_t,一般是unsigned int。pthread_create函数的第一个输出参数tid就代表了线程ID,如果创建成功,tid就返回正确的线程ID。
第二个参数:每个线程都会有很多属性,比如优先级,是否应该称为一个守护进程等,可通过pthread_attr_t来描述,一般不会特殊设置,可以指定这个参数为NULL。
第三个参数:为新线程的入口函数,该函数可以接收一个参数arg,类型为指针,如果想给线程入口函数传入多个值,那么需要把这些值包装成一个结构体,再把结构体的地址作为pthread_create的第四个参数,在线程入口函数内,再将该地址转为该结构体的指针对象。

在新线程的入口函数内,可以调用pthread_self函数返回线程的tid

pthread_t pthread_self(void)

终止线程

终止一个线程最直接的方法就是在父线程内调用pthread_exit函数

void pthread_exit(void *status)

当调用这个函数之后,父线程会等待其他所有的子线程终止,之后父线程自己终止。

也可以通过调用pthread_cancel来主动终止一个子线程,和pthread_exit不同的是,它可以指定某个子线程终止。

int pthread_cancel(pthread_t tid)

回收已终止线程的资源

通过调用pthread_join回收已终止线程的资源。

int pthread_join(pthread_t tid, void ** thread_return)

当调用pthread_join时,主线程会阻塞,直到对应tid的子线程自然终止。和pthread_cancel不同的是,它不会强迫子线程终止。

分离线程

一个线程的重要属性就是可结合的,或者是可分离的。一个可结合的线程是能够被其他线程杀死和回收资源的;而一个分离的线程不能被其他线程杀死或回收资源。一般来说,默认的属性是可结合的。
可通过调用pthread_detach函数来分离一个线程:

int pthread_detach(pthread_t tid)

在高并发的例子里,每个连接都由一个线程单独处理,在这种情况下,服务器程序并不需要对每个子线程进行终止,这样的话,每个子线程可以在入口函数开始的地方,把自己设置为分离的,这样就能在它终止后自动回收相关的线程资源了,就不需要调用 pthread_join 函数了。

每个连接一个线程处理

每次有新的连接到达后,就创建一个新线程

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/poll.h> 
#include <sys/socket.h>
#include <netinet/in.h>

#define SERV_PORT 43211
#define LISTENQ 1024
#define INIT_SIZE 128
#define MAXLINE 1024
#define MAX_LINE 16384

extern void loop_echo(int);

int tcp_server_listen(int port) {
    int listenfd;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(port);

    int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
    if (rt1 < 0) {
        perror( "bind failed ");
        return -1;
    }

    int rt2 = listen(listenfd, LISTENQ);
    if (rt2 < 0) {
        perror("listen failed ");
        return -1;
    }

    signal(SIGPIPE, SIG_IGN);

    return listenfd;
}

void pthread_run(void *arg)
{
    pthread_detach(pthread_self());
    int fd = (int)arg;
    loop_echo(fd);
}

int main(int argc, char* argv[])
{
    int listen_fd = tcp_server_listen(SERV_PORT);
    pthread_t tid;

    while(1)
    {
        struct sockaddr_storage ss;
        socklen_t slen = sizeof(ss);
        int fd = accept(listen_fd, (struct sockaddr *)&ss, &slen);
        if(fd < 0)
        {
            perror("accept failed");
            return -1;
        }
        else
        {
            pthread_create(&tid, NULL, &pthread_run, (void *)fd);
        }
    }
}

在新线程入口函数thread_run里,使用了pthread_detach方法,将子线程转变为分离的,意味着子线程独自负责线程资源的回收。

loop_ehco程序如下,在接收客户端的数据后,再编码回送回去

#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/poll.h> 
#include <sys/socket.h>
#include <netinet/in.h>

#define SERV_PORT 43211
#define LISTENQ 1024
#define INIT_SIZE 128
#define MAXLINE 1024


#define MAX_LINE 16384

char rot13_char(char c) {
    if ((c >= 'a' && c <= 'm') || (c >= 'A' && c <= 'M'))
        return c + 13;
    else if ((c >= 'n' && c <= 'z') || (c >= 'N' && c <= 'Z'))
        return c - 13;
    else
        return c;
}

void loop_echo(int fd) {
    char outbuf[MAX_LINE + 1];
    size_t outbuf_used = 0;
    ssize_t result;
    while (1) {
        char ch;
        result = recv(fd, &ch, 1, 0);

        //断开连接或者出错
        if (result == 0) {
            break;
        } else if (result == -1) {
            perror("read error");
            break;
        }

        if (outbuf_used < sizeof(outbuf)) {
            outbuf[outbuf_used++] = rot13_char(ch);
        }

        if (ch == '\n') {
            send(fd, outbuf, outbuf_used, 0);
            outbuf_used = 0;
            continue;
        }
    }
}

构建线程池处理多个连接

上述程序虽可以正常工作,但如果并发连接过多,就会引起线程的频繁创建和销毁,虽然说线程切换上下文开销不大,但这般频繁的创建销毁也还是会带来不小的开销。
可以使用预创建线程池的方式进行优化。在服务器启动时,可以先按照固定大小预创建多个线程,当有新连接建立时,往连接字队列里放置这个新连接描述字,线程池里的线程负责从连接字队列中取出连接描述字进行处理。

程序的关键在于连接字队列的设计,因为既有往队列中放置描述符的操作,也有从队列中取出描述符的操作。
需要引入两个重要概念,一个是锁mutex,一个是条件变量condition。加锁就是其他线程不能进入;条件变量则是在多个线程需要交互的情况下,用来线程间同步的原语。

#include <stdio.h>
#include <pthread.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <signal.h>
#include <errno.h>
#include <sys/poll.h> 
#include <sys/socket.h>
#include <netinet/in.h>

#define  SERV_PORT 43211
#define  THREAD_NUMBER      4
#define  BLOCK_QUEUE_SIZE   100
#define  LISTENQ 1024
 
extern void loop_echo(int);

typedef struct
{
    /* data */
    pthread_t thread_tid; //thread ID 
    long thread_count; // connections handled
}Thread;

Thread *thread_array;

typedef struct {
    int number; //队列里的描述字最大个数
    int *fd;    //数组指针
    int front;  //当前队列的头位置
    int rear;   //当前队列的尾位置
    pthread_mutex_t mutex;  //锁
    pthread_cond_t cond;    //条件变量
}block_queue;


int tcp_server_listen(int port) {
    int listenfd;
    listenfd = socket(AF_INET, SOCK_STREAM, 0);

    struct sockaddr_in server_addr;
    bzero(&server_addr, sizeof(server_addr));
    server_addr.sin_family = AF_INET;
    server_addr.sin_addr.s_addr = htonl(INADDR_ANY);
    server_addr.sin_port = htons(port);

    int on = 1;
    setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, &on, sizeof(on));

    int rt1 = bind(listenfd, (struct sockaddr *) &server_addr, sizeof(server_addr));
    if (rt1 < 0) {
        perror( "bind failed ");
        return -1;
    }

    int rt2 = listen(listenfd, LISTENQ);
    if (rt2 < 0) {
        perror("listen failed ");
        return -1;
    }

    signal(SIGPIPE, SIG_IGN);

    return listenfd;
}

//初始化队列
void block_queue_init(block_queue *blockQueue, int number)
{
    blockQueue->number = number;
    blockQueue->fd = calloc(number,sizeof(int));
    blockQueue->front = blockQueue->rear = 0;
    pthread_mutex_init(&blockQueue->mutex, NULL);
    pthread_cond_init(&blockQueue->cond, NULL);
}

//往队列里放置一个描述字fd
void block_queue_push(block_queue *blockQueue, int fd)
{
    //一定要先加锁,因为有多个线程需要读写队列
    pthread_mutex_lock(&blockQueue->mutex);
    //将描述字放到队列尾的位置
    blockQueue->fd[blockQueue->rear] = fd;
    //如果已经到最后,重置尾的位置
    if(++blockQueue->rear == blockQueue->number)
    {
        blockQueue->rear = 0;
    }
    printf("push fd %d\n",fd);
    //通知其他等待度的线程,有新的连接字符等待处理
    pthread_cond_signal(&blockQueue->cond);
    //解锁
    pthread_mutex_unlock(&blockQueue->mutex);

}

//从队列里独处描述字进行处理
int block_queue_pop(block_queue *blockQueue)
{
    //加锁
    pthread_mutex_lock(&blockQueue->mutex);
    //判断队列里没有新的连接字可以处理,就一直条件等待,直到有新的连接字入队列
    while(blockQueue->front == blockQueue->rear)
    {
        pthread_cond_wait(&blockQueue->cond, &blockQueue->mutex);
    }
    //取出队列头的连接字
    int fd = blockQueue->fd[blockQueue->front];
    //如果已经到最后,重置头的位置
    if(++blockQueue->front == blockQueue->number)
    {
        blockQueue->front = 0;
    }
    printf("pop fd %d",fd);
    //解锁
    pthread_mutex_unlock(&blockQueue->mutex);
    //返回连接字
    return fd;
}
void thread_run(void *arg)
{
    pthread_t tid = pthread_self();
    pthread_detach(tid);

    block_queue *blockQueue = (block_queue*)arg;
    while(1)
    {
        int fd = block_queue_pop(blockQueue);
        printf("get fd in thread, fd = %ld, tid = %ld\n",fd, tid);
        loop_echo(fd);
    }
}

int main(int argc, char *argv[])
{
    int listen_fd = tcp_server_listen(SERV_PORT);

    block_queue blockQueue;
    block_queue_init(&blockQueue, BLOCK_QUEUE_SIZE);

    thread_array = calloc(THREAD_NUMBER, sizeof(Thread));
    int i;
    for(i = 0; i < THREAD_NUMBER; i++)
    {
        pthread_create(&(thread_array[i].thread_tid), NULL, &thread_run, (void *)&blockQueue);
    }

    while(1)
    {
        struct sockaddr_storage ss;
        socklen_t slen = sizeof(ss);
        int fd = accept(listen_fd, (struct sockaddr *)&ss, &slen);
        if(fd < 0)
        {
            perror("accept failed");
            return -1;
        }
        else
        {
            block_queue_push(&blockQueue, fd);
        }
    }
    return 0;
}

PS:
记得对操作进行加锁和解锁,通过pthread_mutex_lock和pthread_mutex_unlock来完成。
当工作线程没有描述字可用时,需要等待,通过调用pthread_cond_wait,所有的工作线程等待有新的描述字可达。

posted @ 2022-03-27 23:38  牛犁heart  阅读(117)  评论(0编辑  收藏  举报