非常精简的Linux线程池实现(一)——使用互斥锁和条件变量
线程池的含义跟它的名字一样,就是一个由许多线程组成的池子。
有了线程池,在程序中使用多线程变得简单。我们不用再自己去操心线程的创建、撤销、管理问题,有什么要消耗大量CPU时间的任务通通直接扔到线程池里就好了,然后我们的主程序(主线程)可以继续干自己的事去,线程池里面的线程会自动去执行这些任务。
另一方面,线程池提升了多线程程序的性能。我们不需要在大量任务需要执行时现创建大量线程,然后在任务结束时又销毁大量线程,因为线程池里面的线程都是现成的而且能够重复使用。一个理想的线程池能够合理地动态调节池内线程数量,既不会因为线程过少而导致大量任务堆积,也不会因为线程过多了而增加额外的系统开销。
线程池看上去很神奇的样子,那它是怎么实现的呢?线程这么虚渺在的东西也能像有形的物品一样圈在一个池子里?在只知道线程池这个名字的时候,我心里的疑惑就是这样的。
其实线程池的原理非常简单,它就是一个非常典型的生产者消费者同步问题。如果不知道我说的这个XXX问题也不要紧,我下面就解释。
根据刚才描述的线程池的功能,可以看出线程池至少有两个主要动作,一个是主程序不定时地向线程池添加任务,另一个是线程池里的线程领取任务去执行。且不论任务和执行任务是个什么概念,但是一个任务肯定只能分配给一个线程执行。
这样就可以简单猜想线程池的一种可能的架构了:主程序执行入队操作,把任务添加到一个队列里面;池子里的多个工作线程共同对这个队列试图执行出队操作,这里要保证同一时刻只有一个线程出队成功,抢夺到这个任务,其他线程继续共同试图出队抢夺下一个任务。所以在实现线程池之前,我们需要一个队列,我为这个线程池配备的队列单独放到了另一篇博客一个通用纯C队列的实现中。
这里的生产者就是主程序,生产任务(增加任务),消费者就是工作线程,消费任务(执行、减少任务)。因为这里涉及到多个线程同时访问一个队列的问题,所以我们需要互斥锁来保护队列,同时还需要条件变量来处理主线程通知任务到达、工作线程抢夺任务的问题。如果不熟悉条件变量,我在另一篇博客Linux C语言多线程库Pthread中条件变量的的正确用法逐步详解中作了详细说明。
准备工作都差不多了,可以开始设计线程池了。一个最简单线程池应该有什么功能呢?对于使用者来说,除了创建和销毁线程池,最简单的情况下只需要一个功能——添加任务。对于线程池自己来说,最简单的情况下不需要动态调节线程数量,不需要考虑线程同步、线程死锁等等一大堆麻烦的问题。所以最后的线程池API定义为:
- //thread_pool.h
- #ifndef THREAD_POOL_H_INCLUDED
- #define THREAD_POOL_H_INCLUDED
- typedef struct thread_pool *thread_pool_t;
- thread_pool_t thread_pool_create(unsigned int thread_count);
- void thread_pool_add_task(thread_pool_t pool, void* (*routine)(void *arg), void *arg);
- void thread_pool_destroy(thread_pool_t pool);
- #endif //THREAD_POOL_H_INCLUDED
创建线程池时指定线程池中应该固定包含多少工作线程,添加任务就是向线程池添加一个任务函数指针和任务函数需要的参数——这跟Pthread线程库中的普通线程创建函数pthread_create是一样的。根据这套线程池API,我们使用线程池的应用程序应该是这个套路:
- //test.c
- #include "thread_pool.h"
- #include <stdio.h>
- #include <unistd.h>
- #include <pthread.h>
- void* test(void *arg) {
- int i;
- for(i=0; i<5; i++) {
- printf("tid:%ld task:%ld\n", pthread_self(), (long)arg);
- fflush(stdout);
- sleep(2);
- }
- return NULL;
- }
- int main() {
- long i=0;
- thread_pool_t pool;
- pool=thread_pool_create(2);
- for(i=0; i<5; i++) {
- thread_pool_add_task(pool, test, (void*)i);
- }
- puts("press enter to terminate ...");
- getchar();
- thread_pool_destroy(pool);
- return 0;
- }
上面这个测试程序向线程池添加了5个相同的任务,每个任务耗时10秒,但是线程池中只有2个工作线程,所以程序的运行结果是两个工作线程轮流把5个任务挨个做完。显示到屏幕上就是:前10秒两个工作线程轮流输出自己的线程ID和当前任务的任务号0和1,各输出5次;第二个10秒两个工作线程轮流输出自己的线程ID和当前任务的任务号2和3……
在这期间,主程序输出“press enter to terminate ...”并等待用户输入,任何时候都可以按回车让主程序继续往下,这样会强制终止所有工作线程并销毁线程池,最后程序退出。test程序运行效果截图如下:
最后就是线程池真正的实现了:
- //thread_pool.c
- #include "thread_pool.h"
- #include "queue.h"
- #include <stdlib.h>
- #include <pthread.h>
- struct thread_pool {
- unsigned int thread_count;
- pthread_t *threads;
- queue_t tasks;
- pthread_mutex_t lock;
- pthread_cond_t task_ready;
- };
- struct task {
- void* (*routine)(void *arg);
- void *arg;
- };
- static void cleanup(pthread_mutex_t* lock) {
- pthread_mutex_unlock(lock);
- }
- static void * worker(thread_pool_t pool) {
- struct task *t;
- while(1) {
- pthread_mutex_lock(&pool->lock);
- pthread_cleanup_push((void(*)(void*))cleanup, &pool->lock);
- while(queue_isempty(pool->tasks)) {
- pthread_cond_wait(&pool->task_ready, &pool->lock);
- /*A condition wait (whether timed or not) is a cancellation point ... a side-effect of acting upon a cancellation request while in a condition wait is that the mutex is (in effect) re-acquired before calling the first cancellation cleanup handler.*/
- }
- t=(struct task*)queue_dequeue(pool->tasks);
- pthread_cleanup_pop(0);
- pthread_mutex_unlock(&pool->lock);
- t->routine(t->arg);/*todo: report returned value*/
- free(t);
- }
- return NULL;
- }
- thread_pool_t thread_pool_create(unsigned int thread_count) {
- unsigned int i;
- thread_pool_t pool=NULL;
- pool=(thread_pool_t)malloc(sizeof(struct thread_pool));
- pool->thread_count=thread_count;
- pool->threads=(pthread_t*)malloc(sizeof(pthread_t)*thread_count);
- pool->tasks=queue_create();
- pthread_mutex_init(&pool->lock, NULL);
- pthread_cond_init(&pool->task_ready, NULL);
- for(i=0; i<thread_count; i++) {
- pthread_create(pool->threads+i, NULL, (void*(*)(void*))worker, pool);
- }
- return pool;
- }
- void thread_pool_add_task(thread_pool_t pool, void* (*routine)(void *arg), void *arg) {
- struct task *t;
- pthread_mutex_lock(&pool->lock);
- t=(struct task*)queue_enqueue(pool->tasks, sizeof(struct task));
- t->routine=routine;
- t->arg=arg;
- pthread_cond_signal(&pool->task_ready);
- pthread_mutex_unlock(&pool->lock);
- }
- void thread_pool_destroy(thread_pool_t pool) {
- unsigned int i;
- for(i=0; i<pool->thread_count; i++) {
- pthread_cancel(pool->threads[i]);
- }
- for(i=0; i<pool->thread_count; i++) {
- pthread_join(pool->threads[i], NULL);
- }
- pthread_mutex_destroy(&pool->lock);
- pthread_cond_destroy(&pool->task_ready);
- queue_destroy(pool->tasks);
- free(pool->threads);
- free(pool);
- }
上面的worker函数就是工作线程函数,所有的工作线程都在执行着这个函数。它首先在互斥锁和条件变量的保护下从任务队列中取出一个任务,这个任务实际上是一个函数指针和调用函数所需的参数,所以执行任务就很简单了——用任务参数调用任务函数。函数返回以后,工作线程继续去抢任务。
这里没有处理任务函数的返回值问题,理论上任务函数返回以后线程池应该用某种机制通知主程序,然后主程序获取通过某种手段获取返回值,但这明显不是一个最简单的线程池需要操心的事。实际上,应用程序可以通过全局变量或传入的参数指针,加上额外的线程同步代码解决返回值的通知和获取问题。
还有一点需要注意,最后线程池销毁时会强制终止所有处于撤销点(cacellation
points)的工作线程,如果工作线程正在任务函数中没返回而且任务函数中有非手动创建的撤销点,那么任务函数就会在跑到撤销点时戛然而止,这可能导致意外结果。而如果任务函数中没有任何线程撤销点,那么线程池销毁函数会一直阻塞等待直到任务函数完成后才能终止对应的工作线程并返回。
要正确处理这个问题,线程池使用者必须通过自己的线程同步代码保证调用thread_pool_destroy之前所有任务都已经完成、终止或者取消。
非常精简的Linux线程池实现(一)——使用互斥锁和条件变量的更多相关文章
- linux 线程的同步 二 (互斥锁和条件变量)
互斥锁和条件变量 为了允许在线程或进程之间共享数据,同步时必须的,互斥锁和条件变量是同步的基本组成部分. 1.互斥锁 互斥锁是用来保护临界区资源,实际上保护的是临界区中被操纵的数据,互斥锁通常用于保护 ...
- 进程间通信机制(管道、信号、共享内存/信号量/消息队列)、线程间通信机制(互斥锁、条件变量、posix匿名信号量)
注:本分类下文章大多整理自<深入分析linux内核源代码>一书,另有参考其他一些资料如<linux内核完全剖析>.<linux c 编程一站式学习>等,只是为了更好 ...
- node源码详解(七) —— 文件异步io、线程池【互斥锁、条件变量、管道、事件对象】
本作品采用知识共享署名 4.0 国际许可协议进行许可.转载保留声明头部与原文链接https://luzeshu.com/blog/nodesource7 本博客同步在https://cnodejs.o ...
- linux c 线程间同步(通信)的几种方法--互斥锁,条件变量,信号量,读写锁
Linux下提供了多种方式来处理线程同步,最常用的是互斥锁.条件变量.信号量和读写锁. 下面是思维导图: 一.互斥锁(mutex) 锁机制是同一时刻只允许一个线程执行一个关键部分的代码. 1 . ...
- [转]一个简单的Linux多线程例子 带你洞悉互斥量 信号量 条件变量编程
一个简单的Linux多线程例子 带你洞悉互斥量 信号量 条件变量编程 希望此文能给初学多线程编程的朋友带来帮助,也希望牛人多多指出错误. 另外感谢以下链接的作者给予,给我的学习带来了很大帮助 http ...
- Linux互斥锁、条件变量和信号量
Linux互斥锁.条件变量和信号量 来自http://kongweile.iteye.com/blog/1155490 http://www.cnblogs.com/qingxia/archive/ ...
- linux 互斥锁和条件变量
为什么有条件变量? 请参看一个线程等待某种事件发生 注意:本文是linux c版本的条件变量和互斥锁(mutex),不是C++的. mutex : mutual exclusion(相互排斥) 1,互 ...
- 线程私有数据TSD——一键多值技术,线程同步中的互斥锁和条件变量
一:线程私有数据: 线程是轻量级进程,进程在fork()之后,子进程不继承父进程的锁和警告,别的基本上都会继承,而vfork()与fork()不同的地方在于vfork()之后的进程会共享父进程的地址空 ...
- 【Linux C 多线程编程】互斥锁与条件变量
一.互斥锁 互斥量从本质上说就是一把锁, 提供对共享资源的保护访问. 1) 初始化: 在Linux下, 线程的互斥量数据类型是pthread_mutex_t. 在使用前, 要对它进行初始化: 对于静态 ...
随机推荐
- 【教程】Tomcat 的catalina.out 日志按照自定义日期格式进行切割
本文简单介绍在使用cronolog对tomcat的日志进行自定义日期格式的切割,方便日志的整理和遇到问题日志的排查! 安装cronolog 安装cronolog的方法网上有很多,这里也简单的介绍一下. ...
- 电子邮件-TCP
参考下图,由于电子邮件是最早应用于网络上的服务,而早期网络的不可靠性,才设计了TCP协议,而邮件必须要保证正确传输,而非高速,所以早期的电子邮件协议 全是基于TCP的,一直持续到现在.
- Chrome导出书签瘦身,去除ADD_DATE和ICON
Chrome等浏览器导出的书签是一个html文件,每个链接是一个a标签,由于保存着添加时间和图标显得太臃肿. 原本的样子: 我们利用sublime的正则替换,变成一个清爽的样子. 正则表达式:ADD_ ...
- FHQ Treap及其可持久化与朝鲜树式重构
FHQ Treap,又称无旋treap,一种不基于旋转机制的平衡树,可支持所有有旋treap.splay等能支持的操作(只有在LCT中会比splay复杂度多一个log).最重要的是,它是OI中唯一一种 ...
- 【BZOJ5137】Standing Out from the Herd(后缀自动机)
[BZOJ5137]Standing Out from the Herd(后缀自动机) 题面 BZOJ 洛谷 题解 构建广义后缀自动机 然后对于每个节点处理一下它的集合就好了 不知道为什么,我如果按照 ...
- UVA.1640.The Counting Problem / BZOJ.1833.[ZJOI2010]数字计数(数位DP)
题目链接 \(Description\) 求\([l,r]\)中\(0,1,\cdots,9\)每个数字出现的次数(十进制表示). \(Solution\) 对每位分别DP.注意考虑前导0: 在最后统 ...
- node-webkit开发桌面应用
Node-Webkit能够做什么呢?(打开链接看discuss) github 项目源:https://github.com/rogerwang 导言 node-webkit 是一个很神奇的桌面客户端 ...
- java ftp上载下传 遇到的问题
1.下载文件中文乱码,和下载文件大小为0kb /** * Description: 从FTP服务器下载文件 * * @param url * FTP服务器hostname * @param port ...
- ant design的一些坑
1.在本地修改ant design的某些样式可以生效,但在线上就失效了.比如collapse组件里的箭头图标在本地和在线上的类名有变化,本地类名,线上类名:箭头图标的svg样式在线上会自动添加一个内联 ...
- CentOS 7下的KVM网卡配置为千兆网卡
在KVM下可以生成两种型号的网卡,RTL8139和E1000,其实应该是底层生成不同芯片的网卡,而不是附带宿主机网卡是什么型号就是什么型号的,其中默认为100兆网卡,即RTL8319的螃蟹卡,另一种是 ...