• 网络编程:阻塞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,所有的工作线程等待有新的描述字可达。

  • 相关阅读:
    IHE 官方网址有用资源介绍
    HL7 标准及实现指南 必看的网址
    HL7及PIX相关的测试工具
    hl7 v2.X 版本中RSP_K23消息的构造
    hl7中V2版本的ACK消息的构造
    hl7消息中和时间有关的字段的格式
    解决方案: the selected file is a solution file but was created by a newer version of this application and cannot be opened
    wpf中为DataGrid添加checkbox支持多选全选
    hl7 V2中Message Control ID的含义及应用
    Pix mesa 自动化测试
  • 原文地址:https://www.cnblogs.com/whiteBear/p/16065221.html
Copyright © 2020-2023  润新知