工程背景
- 考虑这么一个情况:为应对某场景的实际需求,要在程序中创建大量线程,并且这些线程的数量和生命周期均不确定,可能方生方死,也可能常驻内存,如何在满足其要求的同时,尽可能降低系统负载?
一个基本事实是,线程的创建和销毁都是需要额外的系统资源的,如果线程的生命周期很短,那么相对于实际干活的时间,来回重复创建和销毁就显得很不划算,但也不能让线程执行完任务之后耗着不走,因此就需要一个比较科学合理的布局,来管控线程,一个比较成熟的方案是:在上述情况下,将线程放入一个类似缓冲区的池子中,对于生命周期很短的任务,那么什么时候需要线程就从池子中捞出来干活,干完活就放回池子且让线程保持整装待命,并通过精巧的设计使得池子中的线程数量可以动态地发生变化,让线程既可以应对并发性需求,又不会浪费系统资源,这种设计思路就是线程池。
线程池的基本想法,是将许多线程,放置一个池子中(实际就一个结构体),只要有任务,就将任务投入池中,这些线程们通过某些机制,及时处理这些任务。为了便于管理,线程池还应当提供诸如初始化线程池、增删线程数量、检测未完成任务的数量、检测正在执行任务的线程的数量、销毁线程池等等基本操作。
逻辑框架
- 要让线程池好用,主要是解决如下两个问题:
如何组织线程?
如何组织任务?
线程组织
-
从上图可知,线程被创建出来之后,都是处于睡眠状态,它们实际上是进入到了条件变量的等待队列中,而任务都被放进一个链表中,被互斥锁保护起来
-
下面是线程池整个的一生
- 创建
- 写遗书(准备好退出处理函数,放了防止在持有一把锁的状态中(执行任务)被杀死)
- 试图持有互斥锁(等待任务)
- 判断是否有任务,若无则进入条件变量中等待任务,若有就进入第五步
- 从任务链表中取一个任务
- 释放互斥锁
- 销毁遗书(将备用的退出处理函数弹栈,避免占内存)
- 执行任务,完毕之后就重回第二步
-
线程池代码案例:
一、分析线程池文件 pool_test/ -> 线程池的源码 thread_pool.c -> 线程池函数接口源码 -> 待会主要分析这个文件 thread_pool.h -> 线程池函数声明、线程池结构体.. main.c -> 简单应用。 线程池接口设计说明书.doc -> 详细说明了线程池中函数的用法。 二、线程池实现过程。 1、 什么是线程池? 线程池就是多个线程组合起来的一个集合,就好像一家公司,由多个员工组成的一个集合。当有任务时,这些线程就会去处理这些任务,当没有任务时,线程就会休息。 2、 如何来描述一个线程池? -> 使用一个结构体。 typedef struct thread_pool { pthread_mutex_t lock; -> 互斥锁 pthread_cond_t cond; -> 条件变量 bool shutdown; -> 当前线程池的标志位 true -> 线程池关闭了, false -> 线程池没有关闭。 struct task *task_list; -> 任务队列的头节点 pthread_t *tids; -> 储存所有线程的ID号 //unsigned max_waiting_tasks; -> 最大等待任务的数目 unsigned waiting_tasks; -> 当前正在等待处理的任务的个数//链表中有多少个结点,每个结点就是一个任务,也就是当前有多少个任务 unsigned active_threads; -> 活跃的线程个数 //表示当前线程池里面有多少条线程 }thread_pool; //任务队列 的结点数据类型---- 也就是说 任务结构体的数据类型 struct task { /* 数据域 */ void *(*do_task)(void *arg); //函数指针 -> 这个任务需要执行的函数,这个函数必须是: void *fun(void *arg) void *arg; -> 传递给执行的函数的参数 /* 指针域 */ struct task *next; -> 指向下一个任务节点的指针。 }; 3、 如何实现初始化? -> init_pool() bool init_pool(thread_pool *pool, unsigned int threads_number) { //1. 初始化线程池互斥锁 pthread_mutex_init(&pool->lock, NULL); //2. 初始化条件变量 pthread_cond_init(&pool->cond, NULL); //3. 初始化标志位为false,代表当前线程池正在运行。 pool->shutdown = false; //4. 初始化任务队列的头节点 pool->task_list = malloc(sizeof(struct task)); //5. 为储存线程ID号申请空间。 pool->tids = malloc(sizeof(pthread_t) * MAX_ACTIVE_THREADS); //第4步与第5步错误判断 if(pool->task_list == NULL || pool->tids == NULL) { perror("allocate memory error"); return false; //初始化线程池失败 } //6. 为线程池任务队列的头节点的指针域赋值NULL pool->task_list->next = NULL; //7. 设置线程池最大任务个数为1000 pool->max_waiting_tasks = MAX_WAITING_TASKS; //8. 当前需要处理的任务为0 pool->waiting_tasks = 0; //9. 初始化线程池中线程池的个数 pool->active_threads = threads_number; //10. 创建线程 int i; for(i=0; i<pool->active_threads; i++) { if(pthread_create(&((pool->tids)[i]), NULL,routine, (void *)pool) != 0) { perror("create threads error"); return false; } } //11. 线程池初始化成功 return true; } 总结:初始化线程池函数其实就是给线程池的结构体赋值,将一些变量赋初值,以及初始化变量,为指针申请空间。 2)如何添加任务呢? -> add_task() bool add_task(thread_pool *pool,void *(*do_task)(void *arg), void *arg) { //1. 为新任务的节点申请空间 struct task *new_task = malloc(sizeof(struct task)); if(new_task == NULL) { perror("allocate memory error"); return false; } //2. 为新节点的数据域与指针域赋值 new_task->do_task = do_task; new_task->arg = arg; new_task->next = NULL; //3. 在添加任务之前,必须先上锁,因为添加任务属于访问临界资源 -> 任务队列 pthread_mutex_lock(&pool->lock); //4. 如果当前需要处理的任务个数>=1000 if(pool->waiting_tasks >= MAX_WAITING_TASKS) { //解锁 pthread_mutex_unlock(&pool->lock); //打印一句话提示一下,太多任务了 fprintf(stderr, "too many tasks.\n"); //释放掉刚刚准备好的新节点 free(new_task); return false; } //5. 寻找任务队列中的最后一个节点 struct task *tmp = pool->task_list; while(tmp->next != NULL) tmp = tmp->next; //从循环中出来时,tmp肯定是指向最后一个节点 //6. 让最后一个节点的指针域指向新节点 tmp->next = new_task; //7. 当前需要处理的任务+1 pool->waiting_tasks++; //8. 添加完毕,就解锁。 pthread_mutex_unlock(&pool->lock); //9. 唤醒条件变量中的一个线程起来做任务 pthread_cond_signal(&pool->cond); return true; } 总结: 插入一个新节点到任务队列的末尾,唤醒条件变量中的一个线程起来做任务。 3)线程例程函数 -> void *routine(void *arg) void *routine(void *arg) { //1. 先接住线程池的地址 thread_pool *pool = (thread_pool *)arg; struct task *p; while(1) { //2. 线程取消例程函数 //将来要是有人要取消我,请先把锁解开,然后再响应取消。 pthread_cleanup_push(handler, (void *)&pool->lock); //3. 上锁 pthread_mutex_lock(&pool->lock); //如果当前线程池没有关闭,并且当前线程池没有任务做 while(pool->waiting_tasks == 0 && !pool->shutdown) { //那么就进去条件变量中睡觉。 pthread_cond_wait(&pool->cond, &pool->lock); //自动解锁 } //要是线程池关闭了,或者有任务做,这些线程就会运行到这行代码 //判断当前线程池是不是关闭了,并且没有等待的任务 if(pool->waiting_tasks == 0 && pool->shutdown == true) { //如果线程池关闭,又没有任务做 //线程那么就会解锁 pthread_mutex_unlock(&pool->lock); //线程走人 pthread_exit(NULL); } //能运行到这里,说明有任务做 //p指向头节点的下一个 p = pool->task_list->next; //让头节点的指针域指向p的下一个节点 pool->task_list->next = p->next; //当前任务个数-1 pool->waiting_tasks--; //解锁 pthread_mutex_unlock(&pool->lock); //删除线程取消例程函数 pthread_cleanup_pop(0); //设置线程不能响应取消 pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, NULL); //执行这个p节点指向的节点的函数 (p->do_task)(p->arg); //设置线程能响应取消 pthread_setcancelstate(PTHREAD_CANCEL_ENABLE, NULL); //释放p对应的空间 free(p); } pthread_exit(NULL); } 总结:线程去任务队列中拿一个节点出来,就执行该任务(过程不能被取消),如果没有任务做,就去睡觉。 4)添加线程 -> add_thread() int add_thread(thread_pool *pool, unsigned additional_threads) { //如果说你想添加0条,则直接返回0。 if(additional_threads == 0) return 0; unsigned total_threads = pool->active_threads + additional_threads; //total_threads = 原本 2条 + 现在再加2条 int i, actual_increment = 0; // i = 2 ; i<4 && i<20; i++ for(i = pool->active_threads; i < total_threads && i < MAX_ACTIVE_THREADS; i++) { if(pthread_create(&((pool->tids)[i]),NULL, routine, (void *)pool) != 0) { perror("add threads error"); if(actual_increment == 0) return -1; break; } actual_increment++; //真正创建线程的条数 } pool->active_threads += actual_increment; //当前线程池线程个数 = 原来的个数 + 实际创建的个数 return actual_increment; //返回真正创建的个数 } 总结: 根据用户的需要去创建线程,但是不能超过20条。 5)删除线程 -> remove_thread() int remove_thread(thread_pool *pool, unsigned int removing_threads) { //1. 如果你想删0条,直接返回。 if(removing_threads == 0) return pool->active_threads; //返回当前剩余的线程数 int remaining_threads = pool->active_threads - removing_threads; // 3 = 5 - 2 // 0 = 5 - 5 // -3 = 5 - 8 remaining_threads = remaining_threads > 0 ? remaining_threads : 1; int i; for(i=pool->active_threads-1; i>remaining_threads-1; i--) { errno = pthread_cancel(pool->tids[i]); //取消这些线程 if(errno != 0) break; } if(i == pool->active_threads-1) //删除失败 return -1; else { pool->active_threads = i+1; //当前实际线程个数 return i+1; } } 总结: 根据用户需要,去取消掉一些线程,但是线程池中线程不能小于1条。 6)销毁线程池。 -> destroy_pool() bool destroy_pool(thread_pool *pool) { //1. 设置线程池关闭标志为真 pool->shutdown = true; pthread_cond_broadcast(&pool->cond); //目的: 就是让线程退出! //2. 接合所有的线程 int i; for(i=0; i<pool->active_threads; i++) { errno = pthread_join(pool->tids[i], NULL); if(errno != 0) { printf("join tids[%d] error: %s\n", i, strerror(errno)); } else printf("[%u] is joined\n", (unsigned)pool->tids[i]); } //3. 释放一些空间 free(pool->task_list); free(pool->tids); free(pool); return true; } 总结: 唤醒所有的小孩,让小孩走人。