多线程编程时,需要通过多个线程来处理多个任务。如果不经过优化,那么最简单的想法应该是每到来一个任务,就创建一个线程用来处理这个任务,当这个任务处理完毕,就销毁这个线程。但是这个简单的想法会带来大量的时空开销。设想,假如有成百上千个任务,为了处理它们,岂不是要创建和销毁线程成百上千次,这显然是不划算的。为了减小创建和销毁线程的开销,很容易就可以想到利用"池“结构来优化。所谓的“池”结构,就是我们预先创建好多个线程。当有任务到来时,把这些任务存储起来,与此同时,让空闲的线程去选择任务执行它。这就是线程池的大致思想。创建的线程自线程池创建以后就一直存在,直到线程池被销毁为止,这样就避免了重复创建销毁的开销。
对于一个初步接触线程池的人来说,线程的执行方式是需要特别注意:
线程的竞态执行: 一些人对于线程池的使用可能有一些误解,它们认为线程池是通过从线程池中选择线程来执行手上的任务。其实更加形象的说法应该是我们将任务放入线程池的任务队列,然后线程池中的线程就会自动竞争执行任务。为了达到这一点,我们可以把任务队列视为临界资源,利用生成者-消费者模型来解决。添加任务的程序是生产者,工作线程是消费者。所以工作线程一共有两种状态
- 运行态:取到任务以后开始执行。
- 阻塞态:没取到任务,可能是因为没有获得互斥锁,也可能是因为当前任务队列中没有任务可以执行。
线程池的数据结构和提供的接口:
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
进阶:可以动态调整线程数量的线程池(待续)。