之前学习了互斥锁和条件变量,为了对它们更加熟悉,于是想到了可以学习一下线程池的实现。

线程池,顾名思义,就是由线程组成的一个“池子”。在程序一开始可以直接申请若干个线程,当有任务到来时,将任务交给线程池去完成。这样可以避免频繁的创建和销毁线程,提高运行效率。

线程池的工作模式和“生产者-消费者”问题十分相似。线程池中的线程作为消费者,去消费到来的任务。而主线程也就是控制线程不断向线程池中添加任务。

线程池的数据结构

线程池主要需要用到以下两个结构体:

  1. 任务结构体:
1
2
3
4
typedef struct{
    void (*function)(void *);//函数指针
    void *arg;//函数参数
}threadpool_task_t;

主线程不断向线程池中加入任务,也就是上面的任务结构体。

  1. 线程池结构体
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
  struct threadpool_t{
      pthread_mutex_t lock;  
      pthread_cond_t cond;

      pthread_t *threads;
      threadpool_task_t *queue;
  
      int thread_count; //线程池初始化时的线程数
      int queue_size; //任务队列大小
      int head; //任务队列队头
      int tail; //任务队列队尾
      int count; //当前任务队列中的任务数目
      int shutdown; //是否停止线程池
      int started; //当前启动的线程数目
  };

首先线程池的结构体对于线程池中每个线程是共享的,为了保护共享变量,必须要为该线程池结构体配备互斥锁和条件变量。

需要一个循环队列作为任务队列,来保存到来的但是还没有执行的任务,在本例中使用数组来模拟循环队列,这个数组的类型是上面任务结构体的类型。这样就可以将需要执行的函数和函数参数保存起来。当有任务被执行完即有空闲线程时就可以从任务队列中过的任务函数和函数参数。

线程池操作API

  1. 创建线程池 创建线程池需要指定线程池有多少个线程,还有队列大小,该函数主要是对线程池参数进行初始化。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
threadpool_t *threadpool_create(int thread_count,int queue_size)
{
    threadpool_t *pool;
    int i;
    
    if(thread_count<=0||thread_count>MAX_THREADS||queue_size<=0||queue_size>MAX_QUEUE)
        return NULL;
    if((pool=(threadpool_t *)malloc(sizeof(threadpool_t)))==NULL)
        printError(pool);

    pool->thread_count=0;
    pool->queue_size=queue_size;
    pool->head=pool->tail=pool->count=0;
    pool->shutdown=0;
    pool->started=0;

    pool->threads=(pthread_t *)malloc(sizeof(pthread_t)*thread_count);
    pool->queue=(threadpool_task_t *)malloc(sizeof(threadpool_task_t)*queue_size);

    if((pthread_mutex_init(&(pool->lock),NULL)!=0)||(pthread_cond_init(&(pool->cond),NULL)!=0))
        printError(pool);

    //start worker
    for(i=0;i<thread_count;++i){
        if(pthread_create(&(pool->threads[i]),NULL,threadpool_thread,(void *)pool)!=0){
            threadpool_destroy(pool,0);//如果创建线程失败就销毁线程池,然后返回空指针
            return NULL;
        }
        pool->thread_count++;
        pool->started++;
    }
    return pool;
}

在创建线程池时,pthread_create()会传入以下函数来创建线程。threadpool_thread函数参数为线程池结构体的指针,首先要从传入的线程池结构体指针中或者当前线程池参数,如果线程池当前的任务队列为空,该线程就会调用pthread_cond_wait函数阻塞等待。直到有任务被加入到任务队列,就会唤醒等待着的线程来执行任务。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
static void *threadpool_thread(void *threadpool)
{
    threadpool_t *pool=(threadpool_t *)threadpool;
    threadpool_task_t task;
    while(1){
        pthread_mutex_lock(&(pool->lock));
        while((pool->count==0)&&(!pool->shutdown)){
            pthread_cond_wait(&(pool->cond),&(pool->lock));
        }
        if((pool->shutdown==immediate_shutdown)||((pool->shutdown==grace_shutdown)&&(pool->count==0)))
            break;
        task.function=pool->queue[pool->head].function;
        task.arg=pool->queue[pool->head].arg;

        pool->head=(pool->head+1)%pool->queue_size;
        pool->count--;
       
        pthread_mutex_unlock(&(pool->lock));
        
        (*(task.function))(task.arg);
    }

    pool->started--;
    pthread_mutex_unlock(&(pool->lock));
    pthread_exit(NULL);
    return NULL;
}

  1. 向线程池中添加任务

向线程池添加任务将会改变线程池结构体中的值,在多线程环境下为了保证每次对线程池结构体修改的结果是有效的,必须要加上互斥锁,防止多个线程同时访问造成混乱。在添加完任务后,通过线程池中的条件变量来通知线程可以获取任务执行。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
int threadpool_add(threadpool_t *pool,void (*func)(void *),void *arg)
{
    int err=0;
    int next;
    if(pool==NULL||func==NULL)
        return threadpool_invalid;

    if((pthread_mutex_lock(&(pool->lock))!=0)){
        return threadpool_lock_failure;
    }
    //数组实现环形队列
    next=(pool->tail+1)%pool->queue_size;
    
    do{
        if(pool->count==pool->queue_size){
            err=threadpool_queue_full;
            break;
        }
        if(pool->shutdown){
            err=threadpool_shutdown;
            break;
        }

        pool->queue[pool->tail].function=func;
        pool->queue[pool->tail].arg=arg;
        pool->tail=next;
        pool->count++;
        //添加完任务后,通知阻塞的线程可以从任务队列中取得任务来执行        
        if(pthread_cond_signal(&(pool->cond))!=0){
            err=threadpool_lock_failure;
            break;
        }
    }while(0);
    if(pthread_mutex_unlock(&(pool->lock))!=0)
        err=threadpool_lock_failure;
    return err;
}
  1. 销毁线程池 主线程会对所有阻塞着的线程发出信号,唤醒所有阻塞的线程,再调用pthread_join函数回收所有开启的线程。线程被回收后调用ThreadPool_free函数释放初始化时申请的内存、互斥量和条件变量。
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
int threadpool_destroy(threadpool_t *pool)
{
    int i,err=0;
    if(pool==NULL)
        return threadpool_invalid;
    if(pthread_mutex_lock(&(pool->lock))!=0)
        return threadpool_lock_failure;
    do{
        if(pool->shutdown){
            err=threadpool_shutdown;
            break;
        }
        pool->shutdown=(grace_shutdown)?grace_shutdown:immediate_shutdown;
        
        if((pthread_cond_broadcast(&(pool->cond))!=0)||(pthread_mutex_unlock(&(pool->lock))!=0)){
            err=threadpool_lock_failure;
            break;
        }

        for(int i=0;i<pool->thread_count;++i){
            if(pthread_join(pool->threads[i],NULL)!=0)
                err=threadpool_thread_failure;
        }
    }while(0);
    if(!err)
        threadpool_free(pool);
    return err;
}

int threadpool_free(threadpool_t *pool)
{
    if(pool==NULL||pool->started>0)
        return -1;
    if(pool->threads){
        free(pool->threads);
        free(pool->queue);
        pthread_mutex_lock(&(pool->lock));
        pthread_mutex_destroy(&(pool->lock));
        pthread_cond_destroy(&(pool->cond));
    }
    free(pool);
    return 0;
}

总结

线程池实现思路:

  • 首先初始化线程池,得到空的任务队列。创建出很多个线程阻塞等待任务到来。
  • 线程执行函数中存在while(1)循环,当有任务被添加到线程池时线程唤醒,否则阻塞等待。
  • 添加任务时调用pthread_cond_signal函数,唤醒某个阻塞的线程,来执行添加进去的任务
  • 销毁线程池时,以调用pthread_cond_broadcast函数唤醒所有线程,然后回收线程资源,释放线程池申请的动态内存。

参考资料