概述
现在进程/线程的创建时间已经大大缩短,但是如果需要频繁的创建进程/线程,并且每个子任务处理时间又非常简短,则进程/线程不断的创建与销毁给系统带来的额外负担也是很大。预分配并发进程或线程技术可用于避免创建与销毁进程/线程所付出的代价,进程/线程一旦创建,就会持续运行。
拿预分配线程来说,设计人员编写主程序时便预先创建一定数目的线程(线程池),当请求到达时,就从线程池中申请一个线程来处理任务,任务完成后,并不是将线程销毁,而是将它返还给线程池,由线程池自行管理。如果线程池中预先分配的线程已经全部分配完毕,但此时又有新的任务请求,则线程池会动态的创建新的线程去适应这个请求。当然,有可能,某些时段应用并不需要执行很多的任务,导致了线程池中的线程大多处于空闲的状态,为了节省系统资源,线程池就需要动态的销毁其中的一部分空闲线程。因此,线程池都需要一个管理者,按照一定的要求去动态的维护其中线程的数目。
线程池创建
线程池的创建包含两个步骤:
- 初始化线程池句柄结构;
- 创建线程池(预创建多个线程到池中)。
首先,使用 threadpool_init() 函数来初始化线程池句柄:
#include "threadpool.h" struct ThreadPool *threadpool_init(); 返回值:若成功则返回一个指向新分配线程池句柄的指针,否则返回 NULL
迄今为止,我们仅仅分配和初始化了一个结构。我们仍然需要使用线程池句柄的成员函数 create() 创建实际的线程池。
#include "threadpool.h" int create(struct ThreadPool *tpl, unsigned long long thnum); 返回值:若成功则返回 0,否则返回 -1
线程池使用了类似于 C++ 面向对象的设计,使用成员函数这一术语并不准确,其实 create() 实际上是线程池句柄结构的一个成员(函数指针)。
创建线程池的一个例子(有7个线程):
#include "threadpool.h" int main(int argc, char *argv[]) { int ret; struct ThreadPool *tpl; if ((tpl = threadpool_init()) == NULL) { fprintf(stdout, "threadpool init error.\n"); exit(0); } /** create a threadpool with 7 threads */ if ((ret = tpl->create(tpl, 7)) != 0) { fprintf(stdout, "threadpool create error, may be memory is not enough.\n"); threadpool_destroy(tpl); exit(0); } /* ... */ exit(0); }
销毁线程池句柄
#include "threadpool.h" void threadpool_destroy(struct ThreadPool *tpl); 返回值:无
把任务交给线程池
成功创建线程池后,现在就可以把任务交给线程池来完成了。分三步可以完成这一过程:
- 获取一个空任务;
- 设置任务内容(工作函数);
- 提交任务;
为什么要分三步呢?因为线程池为了满足各种需要,这样不仅减少提交任务冗长的参数,还可以提高线程池任务管理的灵活性。
#include "threadpool.h" struct tpl_task *get_task(struct ThreadPool *tpl, int timeout); 返回值:若成功则返回 tpl_task 句柄,否则返回 NULL int submit_task(struct ThreadPool *tpl, struct tpl_task *task); 返回值:若成功则返回 0,否则返回 -1
先调用 get_task() 函数,然后设置任务相关信息,比如优先级、任务参数、任务函数等。这就好比去银行存钱/取钱,先要取得一张排队凭证/号码,如果你是 VIP 客户,可以获得高优先级。
调用 submit_task() 函数将任务提交给线程池。
get_task() 函数,指定不同的参数 timeout ,行为会有些不同。
- 如果 timeout = -1,则调用线程一直阻塞直到获取空闲任务;
- 如果 timeout = 0,有空闲任务则返回,没有则立即返回 NULL;
- 如果 timeout > 0,同 timeout = -1,只不过最多等待 timeout 秒;
线程池退出
退出线程池很容易,只要调用如下任一函数即可。
#include "threadpool.h" void detach(struct ThreadPool *tpl); void close(struct ThreadPool *tpl); 返回值:无
这两个函数的区别是,detach() 函数等待线程池中的所有任务完成后(包括池中所有线程退出)才返回。close() 函数不等待任务完成,直接让池中所有工作线程退出后返回。
线程池选项
在调用 threadpool_init() 之后,tpl->create() 之前,可以调用 threadpool_option() 修改线程池的默认选项。
#include "threadpool.h" int threadpool_option(struct ThreadPool *tpl, int option, const void *argument); 返回值:成功返回 0
ThreadPool 结构体对调用程序是不透明的,也就是说调用程序并不需要了解内部结构的任何细节。下表总结了线程池的部分属性:
ID | 选项 | 参数 | 说明 |
1 | TPLOPT_THREAD_STACKSIZE | size_t | 工作线程堆栈大小 |
2 | TPLOPT_ENABLE_DEBUG | NULL | 设置调试模式 |
我们来看下例子:
设置工作线程堆栈大小例子: size_t stacksize = 2*1024*1024; /** 2M */ if ((ret = threadpool_option(tpl, TPLOPT_THREAD_STACKSIZE, &stacksize)) != 0 ) { fprintf(stdout, "Set thread stack-size error.\n"); exit(0); } 设置调试模式: ret = threadpool_option(tpl, TPLOPT_ENABLE_DEBUG, NULL))
线程池任务管理选项
线程池使用 tpl_task 结构体对任务进行管理,struct tpl_task 有如下成员:
ID | NAME | TYPE | 说明 |
1 | taskid | unsigned long long | 唯一任务 ID |
2 | priority | unsigned long long | 设置任务优先级,默认 0(最小) |
3 | name | char | 设置任务名(可选) |
4 | remark | char | 设置任务说明(可选) |
线程池在创建的同时,会分配一个由 tpl_task 结构体组成的双向循环链表,其长度 ntasknum 由线程池中工作线程数量决定,公式如下:
ntasknum = bthnum * kthnum + athnum
其中 bthnum 是工作线程的数量,kthnum 默认值为 TPL_BOOST_TASK_MULTIPLE,athnum 默认值为 TPL_BOOST_TASK_ADD。kthnum 与 athnum 均可调用 threadpool_option() 进行修改。
也就是说一个有 10 个工作线程的线程池在默认情况下,线程池中有 10 个工作线程、25 个任务,这些任务有 3 种状态:空任务、任务正在被执行、任务等待被执行;工作线程在不断领取新任务并执行,有 2 种状态:等待任务、执行任务。
与任务管理相关宏定义如下:
ID | NAME | 默认值 | 说明 |
1 | TPL_BOOST_TASK_MULTIPLE | 2 | 任务倍数 |
2 | TPL_BOOST_TASK_ADD | 5 | 任务基数 |
3 | TPL_BOOST_TASK_NAME | 64 | 任务名字符串长度 |
4 | TPL_BOOST_TASK_REMARK | 255 | 任务备注字符串长度 |
与任务管理相关高级选项如下:
ID | 选项 | 参数 | 说明 |
1 | TPLOPT_TASK_MULTIPLE | unsigned long long | 任务倍数 |
2 | TPLOPT_TASK_ADD | unsigned long long | 任务基数 |
任务钩子函数
线程池还有一个非常有用的功能,就是可以设置在满足某种条件的时候回调我们自己设置的函数,即钩子函数。
与任务钩子函数相关高级选项如下:
ID | 选项 | 参数 | 说明 |
1 | TPLOPT_HOOK_TASK_START | 任务开始钩子函数 | 任务开始执行时执行指定函数 |
2 | TPLOPT_HOOK_TASK_FINISH | 任务结束钩子函数 | 任务完成时执行指定函数 |
3 | TPLOPT_HOOK_TASK_ABORT | 任务终止钩子函数 | 任务终止时执行指定函数 |
4 | TPLOPT_HOOK_TASK_CANCEL | 任务取消钩子函数 | 任务取消时执行指定函数 |
使用例子:
如何将参数传递给工作函数
参考
目前在网上找的用 C 编写的线程池库代码仅有一个 libthreadpool on sourceforge,但好像并没有维护了,鉴于此,我自己编写一个。供大家分享。