多线程编程时,需要通过多个线程来处理多个任务。如果不经过优化,那么最简单的想法应该是每到来一个任务,就创建一个线程用来处理这个任务,当这个任务处理完毕,就销毁这个线程。但是这个简单的想法会带来大量的时空开销。设想,假如有成百上千个任务,为了处理它们,岂不是要创建和销毁线程成百上千次,这显然是不划算的。为了减小创建和销毁线程的开销,很容易就可以想到利用"池“结构来优化。所谓的“池”结构,就是我们预先创建好多个线程。当有任务到来时,把这些任务存储起来,与此同时,让空闲的线程去选择任务执行它。这就是线程池的大致思想。创建的线程自线程池创建以后就一直存在,直到线程池被销毁为止,这样就避免了重复创建销毁的开销。

 对于一个初步接触线程池的人来说,线程的执行方式是需要特别注意:

  线程的竞态执行: 一些人对于线程池的使用可能有一些误解,它们认为线程池是通过从线程池中选择线程来执行手上的任务。其实更加形象的说法应该是我们将任务放入线程池的任务队列,然后线程池中的线程就会自动竞争执行任务。为了达到这一点,我们可以把任务队列视为临界资源,利用生成者-消费者模型来解决。添加任务的程序是生产者,工作线程是消费者。所以工作线程一共有两种状态

  • 运行态:取到任务以后开始执行。
  • 阻塞态:没取到任务,可能是因为没有获得互斥锁,也可能是因为当前任务队列中没有任务可以执行。

 

线程池的数据结构和提供的接口:

1. 用结构体表示一个任务,结构体中有一个函数指针和一个参数指针,分别表示待执行的任务和对应的参数

2. 因为实现的比较简单,所以线程池中线程的数量是固定的,用一个数组来表示它

3. 任务队列是一个阻塞队列,符合生产者-消费者模型,阻塞队列用数组以及指向首尾的指针实现,同时有两个信号,显示任务数量和可放的任务数量

4. 线程池中有两个变量refuse和shutdown,表示是否添加任务和是否关闭线程池,当线程池销毁线程时,首先拒绝添加任务,当线程池中的所有任务都结束以后,再关闭线程池

5. 线程池共有三个接口,创建线程池,销毁线程池和添加任务

6. 另外一个重要的函数是工作(work)函数,这也是线程池中每一个线程执行的函数

 1 typedef struct Task{  // 定义任务
 2     void(*function)(void*arg);
 3     void*arg;    
 4 }Task;
 5 
 6 typedef struct threadPool{
 7     
 8     int threadNum;           // 线程的数量
 9     int maxTaskNum;          // 最多任务的数量
10     int taskNum;
11     Task*taskQueue;             // 任务数组
12     int queueFront;          // 队列头部
13     int queueRear;           // 队列尾部
14     pthread_t*threads;       // 线程数组
15 
16     pthread_mutex_t locker;  //
17         pthread_cond_t full;     // 已放任务数量
18     pthread_cond_t empty;    // 可放任务数量
19     
20     int refuse;              // 是否拒绝接受任务
21     int shutdown;            // 是否关闭线程池
22 }threadPool;
23 
24 int threadPoolCreate(threadPool**poolAddr,int threadNum,int maxTaskNum);  // 创建线程池
25 int threadPoolDestroy(threadPool*pool);                     // 销毁线程池
26 int threadPoolTaskAdd(threadPool*pool,Task task);                    // 添加任务
27 static void*worker(void*arg);                                               // 任务函数

 

具体的实现方式可以参考:https://github.com/hadisi1993/TinyThreadPool

进阶:可以动态调整线程数量的线程池(待续)