C 实现有追求的线程池 探究
引言
线程池很普通的老话题,讨论的很多.深入的不多,也就那些基础库中才能见到这种精妙完备的技巧.而本文随大流
想深入简述一种高效控制性强的一种线程池实现.
先引入一个概念, 惊群. 简单举个例子. 春天来了, 公园出现了很多麻雀. 而你恰巧有一个玉米粒. 扔出去,
立马无数麻雀过来争抢.而最终只有一只麻雀得到了.而那些没有抢到的麻雀很累.......
编程中惊群,是个很古老的编程话题了.在服务器开发有机会遇到.有兴趣的可以自行搜索, 多数介绍的质量非常高.
而我们今天只讨论线程池中惊群现象.采用的POSIX跨平台的线程库 pthread.
PTW32_DLLPORT int PTW32_CDECL pthread_cond_signal (pthread_cond_t * cond);
上面函数就是线程池中出现惊群来源. 它会激活 pthread_cond_wait 等待态一个或多个线程.
扯一点 要开发多线程强烈看看 <<POSIX 程序设计>> (csdn网速卡没上传上去, 大家自己搜搜,放在手机里看看)
还有一本讲多线程部分也特别好 <<C接口与实现>> 的最后一章.
本文参照
线程池中惊群问题 http://zsxxsz.iteye.com/blog/2028452
(注,上面博主思路很清晰,写了伪代码. 并且讲了避免线程池惊群的想法. 是个真高手,半步宗师)
还参照了很多,写的很好,但都有些错误,就没贴了. 希望本文是上面那篇文章探讨的下篇. 思路一脉相承.
相比上面文章,本文是个大更新. 真代码真实现.并且加了销毁函数.(线程池释放), 简易函数重载(宏).
前言
先说设计
对于C的设计那就是头文件中接口的定义. 首先来介绍一下本文中用到的 schead.h 中一些结构和宏
typedef void (*vdel_f)(void* node);
上面函数指针定义类型就是线程池中注册函数所采用的类型.
还使用了一个结构体判断宏, C中不支持 struct a == struct b的语法判断. 实现了一个
/* * 比较两个结构体栈上内容是否相等,相等返回true,不等返回false * a : 第一个结构体值 * b : 第二个结构体值 * : 相等返回true, 否则false */ #define STRUCTCMP(a, b) \ (!memcmp(&a, &b, sizeof(a)))
本质是比较结构体中栈中内存.这个技巧是不是很巧妙. 内存是编程中一个大问题,很多事都在内存上出了问题.容我再扯一点, 黑一下C++
个人惭愧的觉得C++真的需要一个垃圾回收器了. 这么繁琐的语言还需要自己洞悉内存变化. 太恐怖了.特别是STL源码击垮了多少人的心房.
黑的不好意思, 一个基础过硬独当一面的C++程序员.也好难遇到了.哎C, C++确实有点'过时'了. 入行的太少, 转行的太多了.工资还不高.
好这里就直接到了重头戏 scpthread.h 线程池的接口设计
#ifndef _H_SCPTHREAD #define _H_SCPTHREAD #include <schead.h> /* * 这是个基于 pthread.h 的线程池. 简单方便高效. * 这里使用了头文件 schead.h 也可以省掉,这里只使用了cdel_f 的类型. * typedef void (*vdel_f)(void* arg); * 也自定义了一个类型 threadpool_t 线程池指针类型,也叫作不完整(全)类型. * 只声明不写实现.也是个常用技巧 */ typedef struct threadpool* threadpool_t; /* * 通过这个接口创建线程池对象.后面就可以使用了. * size : 当前线程池中最大的线程个数 * : 返回创建好的线程池值 */ extern threadpool_t sp_new(int size); /* * 在当前线程池中添加待处理的线程对象. * pool : 线程池对象, sp_new 创建的那个 * run : 运行的函数体, 返回值void, 参数void* * arg : 传入运行的参数 * : 没有返回值 */ extern void sp_add(threadpool_t pool, vdel_f run, void* arg); /* * 优化扩展宏,简化操作.唯一恶心的是宏调试难 * _INT_THREADPOOL 是一个简单的大小设置,控制线程池中线程多少 * * sp_CREATE 同样在上面宏帮助下, 少些一个参数. 认为是函数重载 * * sp_ADD 是一个开发技巧,帮助我们 把 void (*)(type* pi) => void (*)(void* pi), * 这样我们写函数定义的时候更方便随意. */ #define _INT_THREADPOOL (128) #define sp_NEW() \ sp_new(_INT_THREADPOOL) #define sp_ADD(pool, run, arg) \ sp_add(pool, (vdel_f)run, arg) /* * 销毁当前线程池,释放内存,并尝试停止线程池中线程. * ppopl : 指向 sp_new创建的对象的指针 * : 没有返回值 */ extern void sp_del(threadpool_t* ppool); #endif // !_H_SCPTHREAD
定义了不完全类型.线程池类型.有创建销毁,添加内容等. 还写了几个充当'函数重载'的宏. 很多人讨厌宏,觉得宏不可取.
但是在你们用函数重载的时候,你想过麻烦吗. 脱离你的IDE,用vi试试函数重载是不是每次都需要看一遍源码,才知道这个函数到底怎么用的.
但确实函数宏,不好理解. 模板宏更不好理解. 而且调试难度仅次于多线程调试了.
到这里接口设计部分已经完工了.没有好的设计, 什么都不是......
正文
先说容易实现的
这里先看看用到的结构部分,首先是任务链表结构和创建
// 线程任务链表 struct threadjob { vdel_f run; //当前任务中要执行函数体,注册的事件 void* arg; //任务中待执行事件的参数 struct threadjob* next; //指向下一个线程任务链表 }; // struct threadjob 结构对象创建 static inline struct threadjob* _new_threadjob(vdel_f run, void* arg) { struct threadjob* job = malloc(sizeof(struct threadjob)); if(!job) CERR_EXIT("malloc struct threadjob is NULL!"); job->run = run; job->arg = arg; job->next = NULL; return job; }
这里对于内存处理方式,采用了C++中new做法, new出错了程序直接崩.上面if(!job)也可以省略.主要看个人理解了.
下面核心结构设计
// 线程结构体,每个线程一个信号量 struct thread { pthread_t tid; //运行的线程id, 在释放的时候用 pthread_cond_t cond; //当前线程的条件变量 struct thread* next; //下一个线程 }; // 线程池类型定义 struct threadpool { int size; //线程池大小,最大线程数限制 int curr; //当前线程池中总的线程数 int idle; //当前线程池中空闲的线程数 pthread_mutex_t mutex; //线程互斥锁 struct thread* threads; //线程条件变量,依赖mutex线程互斥锁 struct threadjob* head; //线程任务链表的表头, head + tail就是一个队列结构 struct threadjob* tail; //线程任务链表的表尾,这个量是为了后插入的后执行 }; // 添加一个等待的 struct thread 对象到 线程池pool中 static void _thread_add(struct threadpool* pool, pthread_t tid) { struct thread* thread = malloc(sizeof(struct thread)); if(!thread) CERR_EXIT("malloc sizeof(struct thread) is error!"); thread->tid = tid; pthread_cond_init(&thread->cond, NULL); thread->next = pool->threads; pool->threads = thread; } // 依据cnd内存地址属性, 删除pool->threads 中指定数据 static void _thread_del(struct threadpool* pool, pthread_cond_t* cnd) { struct thread* head = pool->threads; if(cnd == &head->cond){ pool->threads = head->next; pthread_cond_destroy(&head->cond); free(head); return; } // 下面是处理非头结点删除 while(head->next){ struct thread* tmp = head->next; if(cnd == &tmp->cond){ //找见了,删掉退出 head->next = tmp->next; pthread_cond_destroy(&tmp->cond); free(tmp); break; } head = tmp; } } // 使用了栈内存比较函数,返回对应线程的cond static pthread_cond_t* _thread_get(struct threadpool* pool, pthread_t tid) { struct thread* head = pool->threads; while (head) { if (STRUCTCMP(tid, head->tid)) break; head = head->next; } return &head->cond; }
对于上面的结构是本文核心,就是每个开启的线程都会有一个独有的线程等待变量. 这样pthread_cond_signal 发送信息都是指定发送的.
性能损耗在 _thread_get 上, 这里设计是采用单链表导致每次都要轮序找到指定线程的线程条件变量. 有好想法的同学可以优化.
对于struct threadpool 结构中 struct threadjob *head, *tail; 是个线程任务队列.
struct thread* threads; 是个线程链表. 当前文件中共用struct threadpool 中 mutex一个互斥量.这里说一下,链表是C结构中基础的基础,
所有代码都是围绕它这个结构. 一定要磨练中熟悉提高.对于刚学习的人.
上面代码都是业务代码, 做的好的就是 pthread_cond_destroy 释放条件变量信息. 也许这个函数中没有释放内存, 但推荐和init成对出现.有始有终.
前戏讲完了, 现在讲解其它简单的代码接口实现
/* * 通过这个接口创建线程池对象.后面就可以使用了. * max : 当前线程池中最大的线程个数 * : 返回创建好的线程池值.创建失败返回NULL */ threadpool_t sp_new(int size) { struct threadpool* pool; // 错误判断,有点丑陋, 申请内存并初始化 if((size <= 0) || !(pool = calloc(1, sizeof(struct threadpool)))){ CERR("struct threadpool calloc is error!"); return NULL; } pool->size = size; pthread_mutex_init(&pool->mutex, NULL); return pool; }
上面就是创建接口的实现代码,calloc相比malloc多调用了memset(&src, 0, sizeof(src))清空置零了.
还有一个释放资源函数.这里是允许创建多个线程池.自然要有提供释放函数.
/* * 销毁当前线程池,释放内存,并尝试停止线程池中线程. * ppopl : 指向 sp_new创建的对象的指针 * : 没有返回值 */ void sp_del(threadpool_t* ppool) { struct threadpool* pool; struct thread* thread; struct threadjob* head; if((!ppool) || !(pool = *ppool)) return; //加锁,等待完全占有锁的时候再去释放资源 pthread_mutex_lock(&pool->mutex); //先释放线程 thread = pool->threads; while(thread){ struct thread* next = thread->next; pthread_cancel(thread->tid); pthread_cond_destroy(&thread->cond); free(thread); thread = next; } //再来释放任务列表 head = pool->head; while(head) { struct threadjob* next = head->next; free(head); head = next; } pthread_mutex_unlock(&pool->mutex); //最后要销毁这个使用的线程锁对象 pthread_mutex_destroy(&pool->mutex); *ppool = NULL; }
也许就是多了这个需求原先的代码量多了一半. 需要围绕它让开启的线程能够支持可需求.安全取消等.本文中用到的很多pthread api.
不熟悉的多搜索,多做笔记.不懂多了, 需要的是自己学习.
对于上面释放函数先竞争唯一互斥量,竞争到了那么就开始释放了.先关闭线程后面释放任务列表和线程条件变量资源.
再说核心实现
这部分和上面参照的博文有很多相似之处,大家看了上面代码,看这个应该很好理解. 核心部分就两个函数,一个是线程轮询处理任务的函数.
一个是构建线程池函数. 线程轮序函数如下
// 线程运行的时候执行函数 static void* _consumer(struct threadpool* pool) { struct threadjob* job; int status; pthread_t tid = pthread_self(); pthread_mutex_t* mtx = &pool->mutex; pthread_cond_t* cnd; //设置线程属性, 默认线程属性 允许退出线程 pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); //设置立即取消 pthread_detach(tid); //设置线程分离,自销毁 // 改消费者线程加锁, 并且得到当前线程的条件变量,vdel_f为了linux上消除警告 pthread_cleanup_push((vdel_f)pthread_mutex_unlock, mtx); pthread_mutex_lock(mtx); cnd = _thread_get(pool, tid); __loop: if(pool->head != NULL) { // 有多线程任务,取出数据从下面处理 job = pool->head; pool->head = job->next; if(pool->tail == job) pool->tail = NULL; // 解锁, 允许其它消费者线程加锁或生产线程添加新任务 pthread_mutex_unlock(mtx); // 回调函数,后面再去删除这个任务 job->run(job->arg); free(job); // 新的一轮开始需要重新加锁 pthread_mutex_lock(mtx); goto __loop; } // 这里相当于 if 的 else, pool->first == NULL ++pool->idle; // 调用pthread_cond_wait 等待线程条件变量被通知且自动解锁 status = pthread_cond_wait(cnd, mtx); --pool->idle; if(status == 0) //等待成功了,那就开始轮序处理任务 goto __loop; //到这里是程序出现异常, 进程退出中, 先减少当前线程 --pool->curr; //去掉这个线程链表pool->threads中对应数据 _thread_del(pool, cnd); pthread_mutex_unlock(mtx); pthread_cleanup_pop(0); return NULL; }
对于pthread_cleanup_push 和 pthread_cleanup_pop 也是posix线程的标准用法, 为了在函数取消的时候释放 lock锁.
其中采用强制转换 (vdel_f) 是为了消除linux上gcc编译的警告.因为gcc上 函数只有 void* (*)(void*) 类型.而window上对于上面宏调用的时候强加了转换
/* * C implementation of PThreads cancel cleanup */ #define pthread_cleanup_push( _rout, _arg ) \ { \ ptw32_cleanup_t _cleanup; \ \ ptw32_push_cleanup( &_cleanup, (ptw32_cleanup_callback_t) (_rout), (_arg) ); \ #define pthread_cleanup_pop( _execute ) \ (void) ptw32_pop_cleanup( _execute ); \ }
linux上代码是这样的
/* Install a cleanup handler: ROUTINE will be called with arguments ARG when the thread is canceled or calls pthread_exit. ROUTINE will also be called with arguments ARG when the matching pthread_cleanup_pop is executed with non-zero EXECUTE argument. pthread_cleanup_push and pthread_cleanup_pop are macros and must always be used in matching pairs at the same nesting level of braces. */ # define pthread_cleanup_push(routine, arg) \ do { \ struct __pthread_cleanup_frame __clframe \ __attribute__ ((__cleanup__ (__pthread_cleanup_routine))) \ = { .__cancel_routine = (routine), .__cancel_arg = (arg), \ .__do_it = 1 }; /* Remove a cleanup handler installed by the matching pthread_cleanup_push. If EXECUTE is non-zero, the handler function is called. */ # define pthread_cleanup_pop(execute) \ __clframe.__do_it = (execute); \ } while (0)
其中 struct __pthread_cleanup_frame 结构如下
/* Structure to hold the cleanup handler information. */ struct __pthread_cleanup_frame { void (*__cancel_routine) (void *); void *__cancel_arg; int __do_it; int __cancel_type; };
提升技术最好的办法
1.多看书
2.多写代码,多搜搜,多问问
3.多看别人的好代码, 多临摹源码
4.多创造,多改进,多实战
等这该明白的都明白了,一切都是那样容易,那样的美的时候. 就可以回家种田了. 哈哈.
再补充上面说明一下.为什么用goto, 不喜欢无脑的for(;;) {}, 并且黑屏幕小,vi上太长了换行不好看.
最后测试
那到了测试环节,测试代码 test_spthread.c
#include <scpthread.h> //全局计时器,存在锁问题 static int _old; //简单的线程打印函数 static void _ppt(const char* str) { printf("%d => %s\n", ++_old, str); } //另一个线程测试函数 static void _doc(void* arg) { printf("p = %d, 技术不决定项目的成败!我老大哭了\n", ++_old); } // 测试开启线程量集 #define _INT_THS (10000) int main(void) { int i; //创建线程池 threadpool_t pool = sp_NEW(); //添加任务到线程池中 for(i=0; i<_INT_THS; ++i){ sp_ADD(pool, _ppt, "你为你负责的项目拼命过吗.流过泪吗"); sp_ADD(pool, _doc, NULL); } //等待5s 再结束吧 SLEEPMS(5000); //清除当前线程池资源, 实战上线程池是常驻内存,不要清除. sp_del(&pool); return 0; }
window 上测试截图
linux上 编译代码部分如下
[pirate@wangzhi_test linux_sc_template]$ make cc -g -Wall -D_DEBUG -c -o test_scpthread.o main/test_scpthread.c -I./module/schead/include -I./module/struct/include -I./module/service/include cc -g -Wall -D_DEBUG -o test_scpthread.out test_scpthread.o scpthread.o -lpthread -lm -I./module/schead/include -I./module/struct/include -I./module/service/include
测试结果截图如下
上面最后没有到 20000主要原因是 ++_old 不是线程安全的. 好的到这里我们关于 避免惊群的线程池就已经设计完毕了.
后记
错误难免的,就和烟盒上的吸烟有害健康一样. 哈哈 吸烟有害, 但健康. 有问题交流立马解决.
用到的所有代码.
schead.h
#ifndef _H_SCHEAD #define _H_SCHEAD #include <stdio.h> #include <stdlib.h> #include <stdbool.h> #include <errno.h> #include <string.h> #include <time.h> #include <stdint.h> #include <stddef.h> /* * 1.0 错误定义宏 用于判断返回值状态的状态码 _RF表示返回标志 * 使用举例 : int flag = scconf_get("pursue"); if(flag != _RT_OK){ sclog_error("get config %s error! flag = %d.", "pursue", flag); exit(EXIT_FAILURE); } * 这里是内部 使用的通用返回值 标志 */ #define _RT_OK (0) //结果正确的返回宏 #define _RT_EB (-1) //错误基类型,所有错误都可用它,在不清楚的情况下 #define _RT_EP (-2) //参数错误 #define _RT_EM (-3) //内存分配错误 #define _RT_EC (-4) //文件已经读取完毕或表示链接关闭 #define _RT_EF (-5) //文件打开失败 /* * 1.1 定义一些 通用的函数指针帮助,主要用于基库的封装中 * 有构造函数, 释放函数, 比较函数等 */ typedef void* (*pnew_f)(); typedef void (*vdel_f)(void* node); // icmp_f 最好 是 int cmp(const void* ln,const void* rn); 标准结构 typedef int (*icmp_f)(); /* * c 如果是空白字符返回 true, 否则返回false * c : 必须是 int 值,最好是 char 范围 */ #define sh_isspace(c) \ ((c==' ')||(c>='\t'&&c<='\r')) /* * 2.0 如果定义了 __GNUC__ 就假定是 使用gcc 编译器,为Linux平台 * 否则 认为是 Window 平台,不可否认宏是丑陋的 */ #if defined(__GNUC__) //下面是依赖 Linux 实现,等待毫秒数 #include <unistd.h> #include <sys/time.h> #define SLEEPMS(m) \ usleep(m * 1000) #else // 这里创建等待函数 以毫秒为单位 , 需要依赖操作系统实现 #include <Windows.h> #include <direct.h> // 加载多余的头文件在 编译阶段会去掉 #define rmdir _rmdir /** * Linux sys/time.h 中获取时间函数在Windows上一种移植实现 **tv : 返回结果包含秒数和微秒数 **tz : 包含的时区,在window上这个变量没有用不返回 ** : 默认返回0 **/ extern int gettimeofday(struct timeval* tv, void* tz); //为了解决 不通用功能 #define localtime_r(t, tm) localtime_s(tm, t) #define SLEEPMS(m) \ Sleep(m) #endif /*__GNUC__ 跨平台的代码都很丑陋 */ //3.0 浮点数据判断宏帮助, __开头表示不希望你使用的宏 #define __DIFF(x, y) ((x)-(y)) //两个表达式做差宏 #define __IF_X(x, z) ((x)<z&&(x)>-z) //判断宏,z必须是宏常量 #define EQ(x, y, c) EQ_ZERO(__DIFF(x,y), c) //判断x和y是否在误差范围内相等 //3.1 float判断定义的宏 #define _FLOAT_ZERO (0.000001f) //float 0的误差判断值 #define EQ_FLOAT_ZERO(x) __IF_X(x,_FLOAT_ZERO) //float 判断x是否为零是返回true #define EQ_FLOAT(x, y) EQ(x, y, _FLOAT_ZERO) //判断表达式x与y是否相等 //3.2 double判断定义的宏 #define _DOUBLE_ZERO (0.000000000001) //double 0误差判断值 #define EQ_DOUBLE_ZERO(x) __IF_X(x,_DOUBLE_ZERO) //double 判断x是否为零是返回true #define EQ_DOUBLE(x,y) EQ(x, y, _DOUBLE_ZERO) //判断表达式x与y是否相等 //4.0 控制台打印错误信息, fmt必须是双引号括起来的宏 #ifndef CERR #define CERR(fmt, ...) \ fprintf(stderr,"[%s:%s:%d][error %d:%s]" fmt "\r\n",\ __FILE__, __func__, __LINE__, errno, strerror(errno),##__VA_ARGS__) #endif/* !CERR */ //4.1 控制台打印错误信息并退出, t同样fmt必须是 ""括起来的字符串常量 #ifndef CERR_EXIT #define CERR_EXIT(fmt,...) \ CERR(fmt,##__VA_ARGS__),exit(EXIT_FAILURE) #endif/* !ERR */ #ifndef IF_CERR /* *4.2 if 的 代码检测 * * 举例: * IF_CERR(fd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP), "socket create error!"); * 遇到问题打印日志直接退出,可以认为是一种简单模板 * code : 要检测的代码 * fmt : 必须是""括起来的字符串宏 * ... : 后面的参数,参照printf */ #define IF_CERR(code, fmt, ...) \ if((code) < 0) \ CERR_EXIT(fmt, ##__VA_ARGS__) #endif //!IF_CERR #ifndef IF_CHECK /* * 是上面IF_CERR 的简化版很好用 */ #define IF_CHECK(code) \ if((code) < 0) \ CERR_EXIT(#code) #endif // !IF_CHECK //5.0 获取数组长度,只能是数组类型或""字符串常量,后者包含'\0' #ifndef LEN #define LEN(arr) \ (sizeof(arr)/sizeof(*(arr))) #endif/* !ARRLEN */ //6.0 程序清空屏幕函数 #ifndef CONSOLE_CLEAR #ifndef _WIN32 #define CONSOLE_CLEAR() \ system("printf '\ec'") #else #define CONSOLE_CLEAR() \ system("cls") #endif/* _WIN32 */ #endif /*!CONSOLE_CLEAR*/ //7.0 置空操作 #ifndef BZERO //v必须是个变量 #define BZERO(v) \ memset(&v,0,sizeof(v)) #endif/* !BZERO */ //9.0 scanf 健壮的 #ifndef SAFETY_SCANF #define SAFETY_SCANF(scanf_code,...) \ while(printf(__VA_ARGS__),scanf_code){\ while(getchar()!='\n');\ puts("输入出错,请按照提示重新操作!");\ }\ while(getchar()!='\n') #endif /*!SAFETY_SCANF*/ //10.0 简单的time帮助宏 #ifndef TIME_PRINT #define TIME_PRINT(code) {\ clock_t __st,__et;\ __st=clock();\ code\ __et=clock();\ printf("当前代码块运行时间是:%lf秒\n",(0.0+__et-__st)/CLOCKS_PER_SEC);\ } #endif /*!TIME_PRINT*/ /* * 10.1 这里是一个 在 DEBUG 模式下的测试宏 * * 用法 : * DEBUG_CODE({ * puts("debug start..."); * }); */ #ifndef DEBUG_CODE # ifdef _DEBUG # define DEBUG_CODE(code) code # else # define DEBUG_CODE(code) # endif // ! _DEBUG #endif // !DEBUG_CODE //11.0 等待的宏 是个单线程没有加锁 #define _STR_PAUSEMSG "请按任意键继续. . ." extern void sh_pause(void); #ifndef INIT_PAUSE # ifdef _DEBUG # define INIT_PAUSE() atexit(sh_pause) # else # define INIT_PAUSE() (void)316 /* 别说了,都重新开始吧 */ # endif #endif/* !INIT_PAUSE */ //12.0 判断是大端序还是小端序,大端序返回true extern bool sh_isbig(void); /** * sh_free - 简单的释放内存函数,对free再封装了一下 **可以避免野指针 **pobj:指向待释放内存的指针(void*) **/ extern void sh_free(void** pobj); /** * 获取 当前时间串,并塞入tstr中长度并返回 ** 使用举例 char tstr[64]; sh_times(tstr, LEN(tstr)); puts(tstr); **tstr : 保存最后生成的最后串 **len : tstr数组的长度 ** : 返回tstr首地址 **/ extern int sh_times(char tstr[], int len); /* * 比较两个结构体栈上内容是否相等,相等返回true,不等返回false * a : 第一个结构体值 * b : 第二个结构体值 * : 相等返回true, 否则false */ #define STRUCTCMP(a, b) \ (!memcmp(&a, &b, sizeof(a))) #endif/* ! _H_SCHEAD */
scpthread.h
#ifndef _H_SCPTHREAD #define _H_SCPTHREAD #include <schead.h> /* * 这是个基于 pthread.h 的线程池. 简单方便高效. * 这里使用了头文件 schead.h 也可以省掉,这里只使用了cdel_f 的类型. * typedef void (*vdel_f)(void* arg); * 也自定义了一个类型 threadpool_t 线程池指针类型,也叫作不完整(全)类型. * 只声明不写实现.也是个常用技巧 */ typedef struct threadpool* threadpool_t; /* * 通过这个接口创建线程池对象.后面就可以使用了. * size : 当前线程池中最大的线程个数 * : 返回创建好的线程池值 */ extern threadpool_t sp_new(int size); /* * 在当前线程池中添加待处理的线程对象. * pool : 线程池对象, sp_new 创建的那个 * run : 运行的函数体, 返回值void, 参数void* * arg : 传入运行的参数 * : 没有返回值 */ extern void sp_add(threadpool_t pool, vdel_f run, void* arg); /* * 优化扩展宏,简化操作.唯一恶心的是宏调试难 * _INT_THREADPOOL 是一个简单的大小设置,控制线程池中线程多少 * * sp_CREATE 同样在上面宏帮助下, 少些一个参数. 认为是函数重载 * * sp_ADD 是一个开发技巧,帮助我们 把 void (*)(type* pi) => void (*)(void* pi), * 这样我们写函数定义的时候更方便随意. */ #define _INT_THREADPOOL (128) #define sp_NEW() \ sp_new(_INT_THREADPOOL) #define sp_ADD(pool, run, arg) \ sp_add(pool, (vdel_f)run, arg) /* * 销毁当前线程池,释放内存,并尝试停止线程池中线程. * ppopl : 指向 sp_new创建的对象的指针 * : 没有返回值 */ extern void sp_del(threadpool_t* ppool); #endif // !_H_SCPTHREAD
scpthread.c
#include <scpthread.h> #include <pthread.h> // 线程任务链表 struct threadjob { vdel_f run; //当前任务中要执行函数体,注册的事件 void* arg; //任务中待执行事件的参数 struct threadjob* next; //指向下一个线程任务链表 }; // struct threadjob 结构对象创建 static inline struct threadjob* _new_threadjob(vdel_f run, void* arg) { struct threadjob* job = malloc(sizeof(struct threadjob)); if(!job) CERR_EXIT("malloc struct threadjob is NULL!"); job->run = run; job->arg = arg; job->next = NULL; return job; } // 线程结构体,每个线程一个信号量 struct thread { pthread_t tid; //运行的线程id, 在释放的时候用 pthread_cond_t cond; //当前线程的条件变量 struct thread* next; //下一个线程 }; // 线程池类型定义 struct threadpool { int size; //线程池大小,最大线程数限制 int curr; //当前线程池中总的线程数 int idle; //当前线程池中空闲的线程数 pthread_mutex_t mutex; //线程互斥锁 struct thread* threads; //线程条件变量,依赖mutex线程互斥锁 struct threadjob* head; //线程任务链表的表头, head + tail就是一个队列结构 struct threadjob* tail; //线程任务链表的表尾,这个量是为了后插入的后执行 }; // 添加一个等待的 struct thread 对象到 线程池pool中 static void _thread_add(struct threadpool* pool, pthread_t tid) { struct thread* thread = malloc(sizeof(struct thread)); if(!thread) CERR_EXIT("malloc sizeof(struct thread) is error!"); thread->tid = tid; pthread_cond_init(&thread->cond, NULL); thread->next = pool->threads; pool->threads = thread; } // 依据cnd内存地址属性, 删除pool->threads 中指定数据 static void _thread_del(struct threadpool* pool, pthread_cond_t* cnd) { struct thread* head = pool->threads; if(cnd == &head->cond){ pool->threads = head->next; pthread_cond_destroy(&head->cond); free(head); return; } // 下面是处理非头结点删除 while(head->next){ struct thread* tmp = head->next; if(cnd == &tmp->cond){ //找见了,删掉退出 head->next = tmp->next; pthread_cond_destroy(&tmp->cond); free(tmp); break; } head = tmp; } } // 使用了栈内存比较函数,返回对应线程的cond static pthread_cond_t* _thread_get(struct threadpool* pool, pthread_t tid) { struct thread* head = pool->threads; while (head) { if (STRUCTCMP(tid, head->tid)) break; head = head->next; } return &head->cond; } /* * 通过这个接口创建线程池对象.后面就可以使用了. * max : 当前线程池中最大的线程个数 * : 返回创建好的线程池值.创建失败返回NULL */ threadpool_t sp_new(int size) { struct threadpool* pool; // 错误判断,有点丑陋, 申请内存并初始化 if((size <= 0) || !(pool = calloc(1, sizeof(struct threadpool)))){ CERR("struct threadpool calloc is error!"); return NULL; } pool->size = size; pthread_mutex_init(&pool->mutex, NULL); return pool; } // 线程运行的时候执行函数 static void* _consumer(struct threadpool* pool) { struct threadjob* job; int status; pthread_t tid = pthread_self(); pthread_mutex_t* mtx = &pool->mutex; pthread_cond_t* cnd; //设置线程属性, 默认线程属性 允许退出线程 pthread_setcanceltype(PTHREAD_CANCEL_ASYNCHRONOUS, NULL); //设置立即取消 pthread_detach(tid); //设置线程分离,自销毁 // 改消费者线程加锁, 并且得到当前线程的条件变量,vdel_f为了linux上消除警告 pthread_cleanup_push((vdel_f)pthread_mutex_unlock, mtx); pthread_mutex_lock(mtx); cnd = _thread_get(pool, tid); __loop: if(pool->head != NULL) { // 有多线程任务,取出数据从下面处理 job = pool->head; pool->head = job->next; if(pool->tail == job) pool->tail = NULL; // 解锁, 允许其它消费者线程加锁或生产线程添加新任务 pthread_mutex_unlock(mtx); // 回调函数,后面再去删除这个任务 job->run(job->arg); free(job); // 新的一轮开始需要重新加锁 pthread_mutex_lock(mtx); goto __loop; } // 这里相当于 if 的 else, pool->first == NULL ++pool->idle; // 调用pthread_cond_wait 等待线程条件变量被通知且自动解锁 status = pthread_cond_wait(cnd, mtx); --pool->idle; if(status == 0) //等待成功了,那就开始轮序处理任务 goto __loop; //到这里是程序出现异常, 进程退出中, 先减少当前线程 --pool->curr; //去掉这个线程链表pool->threads中对应数据 _thread_del(pool, cnd); pthread_mutex_unlock(mtx); pthread_cleanup_pop(0); return NULL; } /* * 在当前线程池中添加待处理的线程对象. * pool : 线程池对象, sp_new 创建的那个 * run : 运行的函数体, 返回值void, 参数void* * arg : 传入运行的参数 * : 不需要返回值 */ void sp_add(threadpool_t pool, vdel_f run, void* arg) { struct threadjob* job = _new_threadjob(run, arg); pthread_mutex_t* mtx = &pool->mutex; pthread_mutex_lock(mtx); if(!pool->head) //线程池中没有线程头,那就设置线程头 pool->head = job; else pool->tail->next = job; pool->tail = job; // 有空闲线程,添加到处理任务队列中,直接返回 if(pool->idle > 0){ pthread_mutex_unlock(mtx); // 这是一种算法, 先释放锁后发送信号激活线程,速度快,缺点丧失线程执行优先级 pthread_cond_signal(&pool->threads->cond); } else if(pool->curr < pool->size){ // 没有那就新建线程, 条件不满足那就等待 pthread_t tid; if(pthread_create(&tid, NULL, (void* (*)(void*))_consumer, pool) == 0) ++pool->curr; //添加开启线程的信息 _thread_add(pool, tid); pthread_mutex_unlock(mtx); } } /* * 销毁当前线程池,释放内存,并尝试停止线程池中线程. * ppopl : 指向 sp_new创建的对象的指针 * : 没有返回值 */ void sp_del(threadpool_t* ppool) { struct threadpool* pool; struct thread* thread; struct threadjob* head; if((!ppool) || !(pool = *ppool)) return; //加锁,等待完全占有锁的时候再去释放资源 pthread_mutex_lock(&pool->mutex); //先释放线程 thread = pool->threads; while(thread){ struct thread* next = thread->next; pthread_cancel(thread->tid); pthread_cond_destroy(&thread->cond); free(thread); thread = next; } //再来释放任务列表 head = pool->head; while(head) { struct threadjob* next = head->next; free(head); head = next; } pthread_mutex_unlock(&pool->mutex); //最后要销毁这个使用的线程锁对象 pthread_mutex_destroy(&pool->mutex); *ppool = NULL; }
test_scpthread.c
#include <scpthread.h> //全局计时器,存在锁问题 static int _old; //简单的线程打印函数 static void _ppt(const char* str) { printf("%d => %s\n", ++_old, str); } //另一个线程测试函数 static void _doc(void* arg) { printf("p = %d, 技术不决定项目的成败!我老大哭了\n", ++_old); } // 测试开启线程量集 #define _INT_THS (10000) int main(void) { int i; //创建线程池 threadpool_t pool = sp_NEW(); //添加任务到线程池中 for(i=0; i<_INT_THS; ++i){ sp_ADD(pool, _ppt, "你为你负责的项目拼命过吗.流过泪吗"); sp_ADD(pool, _doc, NULL); } //等待5s 再结束吧 SLEEPMS(5000); //清除当前线程池资源, 实战上线程池是常驻内存,不要清除. sp_del(&pool); return 0; }
Makefile
C = gcc DEBUG = -g -Wall -D_DEBUG #指定pthread线程库 LIB = -lpthread -lm #指定一些目录 DIR = -I./module/schead/include -I./module/struct/include -I./module/service/include #具体运行函数 RUN = $(CC) $(DEBUG) -o $@ $^ $(LIB) $(DIR) RUNO = $(CC) $(DEBUG) -c -o $@ $^ $(DIR) # 主要生成的产品 all:test_cjson_write.out test_csjon.out test_csv.out test_json_read.out test_log.out\ test_scconf.out test_tstring.out test_sctimer.out test_scpthread.out #挨个生产的产品 test_cjson_write.out:test_cjson_write.o schead.o sclog.o tstring.o cjson.o $(RUN) test_csjon.out:test_csjon.o schead.o sclog.o tstring.o cjson.o $(RUN) test_csv.out:test_csv.o schead.o sclog.o sccsv.o tstring.o $(RUN) test_json_read.out:test_json_read.o schead.o sclog.o sccsv.o tstring.o cjson.o $(RUN) test_log.out:test_log.o schead.o sclog.o $(RUN) test_scconf.out:test_scconf.o schead.o scconf.o tree.o tstring.o sclog.o $(RUN) test_tstring.out:test_tstring.o tstring.o sclog.o schead.o $(RUN) test_sctimer.out:test_sctimer.o schead.o sctimer.o $(RUN) test_scpthread.out:test_scpthread.o scpthread.o $(RUN) #产品主要的待链接文件 test_cjson_write.o:./main/test_cjson_write.c $(RUNO) test_csjon.o:./main/test_csjon.c $(RUNO) test_csv.o:./main/test_csv.c $(RUNO) test_json_read.o:./main/test_json_read.c $(RUNO) test_log.o:./main/test_log.c $(RUNO) -std=gnu99 test_scconf.o:./main/test_scconf.c $(RUNO) test_tstring.o:./main/test_tstring.c $(RUNO) test_sctimer.o:./main/test_sctimer.c $(RUNO) test_scpthread.o:./main/test_scpthread.c $(RUNO) #工具集机械码,待别人链接 schead.o:./module/schead/schead.c $(RUNO) sclog.o:./module/schead/sclog.c $(RUNO) sccsv.o:./module/schead/sccsv.c $(RUNO) tstring.o:./module/struct/tstring.c $(RUNO) cjson.o:./module/schead/cjson.c $(RUNO) scconf.o:./module/schead/scconf.c $(RUNO) tree.o:./module/struct/tree.c $(RUNO) sctimer.o:./module/service/sctimer.c $(RUNO) scpthread.o:./module/service/scpthread.c $(RUNO) #删除命令 clean: rm -rf *.i *.s *.o *.out __* log ; ls -hl .PHONY:clean