Linux线程池实现--基于Select复用的回射服务器

来源:互联网 发布:雅米网络兼职骗过的 编辑:程序博客网 时间:2024/06/05 01:11

多线程技术

多线程技术主要解决处理器单元内多个线程执行的问题,它可以使处理器尽量保持忙碌状态。在C/S模式的服务器–客户端通信中应用多线程技术,即是服务器在Accept调用等待客户端连接成功时,为每一个客户端连接创建一个线程来处理客户请求。在这种情况下,每次客户连接时,服务器都会进行线程创建、任务分配;客户请求完成后,服务器还需要销毁线程,这一系列的工作需要消耗很多时间,尤其是在大量用户并发时此矛盾尤为突出。例如,一个用户请求连接,处理器则为它创建一个线程;此时另一个用户请求连接,这时这个用户只能等待处理器完成上一个线程创建之后才能连接成功。

线程池

线程执行时间

线程执行过程分为三个部分:T1:线程创建的时间,T2:线程执行任务所需时间,包括线程的同步等时间;T3线程销毁时间。总计线程完成一项恩物的时间为T1、T2、T3之和。真正处理任务的时间为T2,这样线程开销占总时间的比例为(T1+T3)/(T1+T2+T3),当线程任务处理时间T2较小时,那么开销所占比例将趋近于1,这也正是多线程技术的瓶颈所在。

线程池的优点

线程池的实现着眼于减少T1和T3这两部分时间的开销,使得系统的效率提高。服务器通过预先创建一定数量的工作线程并限制其数量,等到客户端发起连接请求时,直接从线程池分配一个线程用于客户端请求。
1. 可以控制预先产生的线程的数量,也就是线程池的大小,从而达到控制线程对象的内存消耗。
2. 降低系统开销和资源消耗。通过对多个请求重用线程,线程创建和销毁的开销被分配到了多个请求上。
3. 提高系统的响应速度。

线程池的基本原理

线程池主要包括以下几个组成部分:

  • 线程管理器:用于创建并管理线程池
  • 工作线程:线程池中实际执行任务的线程。
  • 任务接口:每个任务必须实现的接口,当可执行任务到来时,线程池中的工作线程将调试执行这个任务。把任务抽象为任务接口,可以做到线程池与具体的任务无关。
  • 任务队列:用来存放没有处理的任务,提供一种缓冲机制。常用队列来实现。

线程池的具体实现

线程池的具体实现借鉴了《线程池技术在并发服务器中的应用》论文所述。

数据结构定义

typedef struct worker{ //任务接口    //回调函数,任务运行时会调用此函数,注意也可声明成其它形式    void *(*process) (void *arg);    void *arg;//回调函数的参数    struct worker *next;}CThread_worker;typedef struct {//线程管理器    pthread_mutex_t queue_lock; //共享资源互斥锁    pthread_cond_t  queue_ready;    CThread_worker *queue_head;//任务队列    int shutdown;//是否销毁线程池    pthread_t *threadid; //工作线程引用    int max_thread_num;    //当前等待队列的任务数目    int cur_queue_size;}CThread_pool;

线程池相关函数

int pool_add_worker (void *(*process) (void *arg), void *arg);void *thread_routine (void *arg);static CThread_pool *pool = NULL;voidpool_init (int max_thread_num){    pool = (CThread_pool *) malloc (sizeof (CThread_pool));    pthread_mutex_init (&(pool->queue_lock), NULL);    pthread_cond_init (&(pool->queue_ready), NULL);    pool->queue_head = NULL;    pool->max_thread_num = max_thread_num;    pool->cur_queue_size = 0;    pool->shutdown = 0;    pool->threadid =    (pthread_t *) malloc (max_thread_num * sizeof (pthread_t));    int i = 0;    for (i = 0; i < max_thread_num; i++)    {        pthread_create (&(pool->threadid[i]), NULL, thread_routine, NULL);    }}/*向线程池中加入任务*/intpool_add_worker (void *(*process) (void *arg), void *arg){    /*构造一个新任务*/    CThread_worker *newworker =    (CThread_worker *) malloc (sizeof (CThread_worker));    newworker->process = process;    newworker->arg = arg;    newworker->next = NULL;/*别忘置空*/    pthread_mutex_lock (&(pool->queue_lock));    /*将任务加入到等待队列中*/    CThread_worker *member = pool->queue_head;    if (member != NULL)    {        while (member->next != NULL)            member = member->next;        member->next = newworker;    }    else    {        pool->queue_head = newworker;    }    assert (pool->queue_head != NULL);    pool->cur_queue_size++;    pthread_mutex_unlock (&(pool->queue_lock));    /*好了,等待队列中有任务了,唤醒一个等待线程;     注意如果所有线程都在忙碌,这句没有任何作用*/    pthread_cond_signal (&(pool->queue_ready));    return 0;}/*销毁线程池,等待队列中的任务不会再被执行,但是正在运行的线程会一直 把任务运行完后再退出*/intpool_destroy (){    if (pool->shutdown)        return -1;/*防止两次调用*/    pool->shutdown = 1;    /*唤醒所有等待线程,线程池要销毁了*/    pthread_cond_broadcast (&(pool->queue_ready));    /*阻塞等待线程退出,否则就成僵尸了*/    int i;    for (i = 0; i < pool->max_thread_num; i++)        pthread_join (pool->threadid[i], NULL);    free (pool->threadid);    /*销毁等待队列*/    CThread_worker *head = NULL;    while (pool->queue_head != NULL)    {        head = pool->queue_head;        pool->queue_head = pool->queue_head->next;        free (head);    }    /*条件变量和互斥量也别忘了销毁*/    pthread_mutex_destroy(&(pool->queue_lock));    pthread_cond_destroy(&(pool->queue_ready));    free (pool);    /*销毁后指针置空是个好习惯*/    pool=NULL;    return 0;}void *thread_routine (void *arg){    printf ("starting thread %d\n", (int)pthread_self ());    while (1)    {        pthread_mutex_lock (&(pool->queue_lock));        /*如果等待队列为0并且不销毁线程池,则处于阻塞状态; 注意         pthread_cond_wait是一个原子操作,等待前会解锁,唤醒后会加锁*/        while (pool->cur_queue_size == 0 && !pool->shutdown)        {            printf ("thread %d is waiting\n", (int)pthread_self ());            pthread_cond_wait (&(pool->queue_ready), &(pool->queue_lock));        }        /*线程池要销毁了*/        if (pool->shutdown)        {            /*遇到break,continue,return等跳转语句,千万不要忘记先解锁*/            pthread_mutex_unlock (&(pool->queue_lock));            printf ("thread %d will exit\n", (int)pthread_self ());            pthread_exit (NULL);        }        printf ("thread %d is starting to work\n", (int)pthread_self ());        /*assert是调试的好帮手*/        assert (pool->cur_queue_size != 0);        assert (pool->queue_head != NULL);        /*等待队列长度减去1,并取出链表中的头元素*/        pool->cur_queue_size--;        CThread_worker *worker = pool->queue_head;        pool->queue_head = worker->next;        pthread_mutex_unlock (&(pool->queue_lock));        /*调用回调函数,执行任务*/        (*(worker->process)) (worker->arg);        free (worker);        worker = NULL;    }    /*这一句应该是不可达的*/    pthread_exit (NULL);}

使用线程池的回射服务器

服务器端:注意这里使用线程池+select函数对读写进行监听,如果没有使用select函数,当有多个客户请求回射时,线程将会阻塞在最先请求回射的标准输入上,其他线程的回射请求将不能得到满足。

#include "pthread_pool.h"#include "unp.h"#include "wraplib.c"#include "readline.c"#define  MAX_THREAD 30  //定义线程数大小void *process(void *arg){     int connfd = *((int *)arg);    if(connfd > 0 )        printf("connect ok\n");    else        printf("connect error\n");    return NULL;}int main(int argc, char **argv) {    int                 i, maxi, maxfd, listenfd, connfd, sockfd;    int                 nready, client[FD_SETSIZE];    ssize_t             n;    fd_set              rset, allset;    char                buf[MAXLINE], str[MAXLINE];    socklen_t           clilen;    struct sockaddr_in  cliaddr, servaddr;    listenfd = Socket(AF_INET, SOCK_STREAM, 0);    bzero(&servaddr, sizeof(servaddr));    servaddr.sin_family = AF_INET;    servaddr.sin_port = htons(SERV_PORT);    servaddr.sin_addr.s_addr = htonl(INADDR_ANY);    Bind(listenfd, (SA*)&servaddr, sizeof(servaddr));    Listen(listenfd, LISTENQ);    maxfd = listenfd;    maxi = -1;    for(i = 0; i <FD_SETSIZE; i++)        client[i] = -1;    FD_ZERO(&allset);    FD_SET(listenfd, &allset);    signal(SIGCHLD, sig_chld);    pool_init(MAX_THREAD);  //创建线程池    for( ; ; ){        rset = allset;        nready = Select(maxfd+1, &rset, NULL, NULL, NULL);        if(FD_ISSET(listenfd, &rset)){            clilen = sizeof(cliaddr);            connfd = Accept(listenfd, (SA *) &cliaddr, &clilen);            printf("new client %s, port %d\n",            Inet_ntop(AF_INET, &cliaddr.sin_addr, str, sizeof(cliaddr)),                   ntohs(cliaddr.sin_port));            for(i = 0; i < FD_SETSIZE; i++)                if(client[i] < 0){                    client[i] = connfd;                    break;                }            if (i == FD_SETSIZE)                err_quit("too many clients");            FD_SET(connfd, &allset);            if (connfd > maxfd)                maxfd = connfd;         /* for select */            if (i > maxi)                maxi = i;               /* max index in client[] array */            if (--nready <= 0)                continue;        }        for (i = 0; i <= maxi; i++) {            if ( (sockfd = client[i]) < 0)                continue;            if (FD_ISSET(sockfd, &rset)) {                if(pool_add_worker(process, &connfd) == 0) {//加入任务队列                    if ( (n = Read(sockfd, buf, MAXLINE)) == 0) {                    /*4connection closed by client */                        Close(sockfd);                        FD_CLR(sockfd, &allset);                        client[i] = -1;                    } else                        Writen(sockfd, buf, n);                if (--nready <= 0)                    break;              /* no more readable descriptors */            }            }        }    }    return 0;}

客户端:循环对服务器发起多个回射请求

#include    "unp.h"#include    "wrapunix.c"#include    "wrapsock.c"#include    "wraplib.c"#include    "str_cli.c"#include    "wrapstdio.c"#include    "error.c"#include    "readline.c"#include    "writen.c"#include    "connect_nonb.c"//#define count 1020//客户端循环向服务器发起回射请求int main(){    char *argv = "192.168.31.54";    int  sockfd[count];    struct  sockaddr_in servaddr;    int i;//    for(i= 0; i < count; i++)        sockfd[i] = Socket(AF_INET, SOCK_STREAM, 0);    bzero(&servaddr, sizeof(servaddr));    servaddr.sin_family = AF_INET;    servaddr.sin_port = htons(SERV_PORT);    Inet_pton(AF_INET, argv, &servaddr.sin_addr);    for(i = 0; i < count; i++) {        connect_nonb(sockfd[i], (SA*)&servaddr, sizeof(servaddr), 0);        FILE *fp = fopen("/Users/hupac/Public/k.c", "rb+"); //直接回射文件内容        str_cli(fp, sockfd[i]);    }    for(i = 0; i < count; i++) {        Close(sockfd[i]);    }    exit(0);}

总结

  • 客户端循环对服务器发起1020次回射请求(sockfd监听收到select函数FD_SETSIZE上限的制约,最大请求次数只能为1020。这是由于当进程只打开了一个套接字描述符时,这个套接字描述符最小值为3,因为0、1、2在打开时内核自动分配。接着每次回射请求都会讲select第一个参数设定为套接字描述符+1。当工作到第1020次回射请求时,此时套接字描述符为1023,而select函数第一个参数为1023+1==1024,若再发起请求将会受到select函数的限制。
  • 对于进程来说,虚地址空间的大小是固定,且进程中只有一个栈。这个栈必须被所有的线程栈共享。如果应用程序使用了过多的线程,且这些线程栈的累计大小超过了可用的虚地址空间,就需要减少默认的线程栈大小。

操作系统为Mac OS X,内存16GB,处理器2.7 GHz Intel Core i5,通过活动监视器观察得最大线程数目为2048。

线程数\时间(ms) 第一次 第二次 第三次 100 2050777 2032623 2025548 1020 2599732 2613504 2626679 5000(无效值,实际为2048) 3373402 3347533 3301522

实际测量得在上述环境最佳性能在服务器线程池大小为30~40(个线程)处获取,这点让我很疑惑,讲道理不应该是线程池大小大于客户端请求时可以使客户请求得到较快的满足?这个问题有待进一步的探究。

原创粉丝点击