(转)高效线程池之无锁化实现(Linux C)
笔者之前照着通用写法练手写过一个小的线程池版本,最近几天复习了一下,发现大多数线程池实现都离不开锁的使用,如互斥量pthread_mutex*结合条件变量pthread_cond*。众所周知,锁的使用对于程序性能影响较大,虽然现有的pthread_mutex*在锁的申请与释放方面做了较大的优化,但仔细想想,线程池的实现是可以做到无锁化的,于是有了本文。
1.常见线程池实现原理
如上图所示,工作队列由主线程和工作者线程共享,主线程将任务放进工作队列,工作者线程从工作队列中取出任务执行。共享工作队列的操作需在互斥量的保护下安全进行,主线程将任务放进工作队列时若检测到当前待执行的工作数目小于工作者线程总数,则需使用条件变量唤醒可能处于等待状态的工作者线程。当然,还有其他地方可能也会使用到互斥量和条件变量,不再赘述。
2.无锁化线程池实现原理
为解决无锁化的问题,需要避免共享资源的竞争,因此将共享工作队列加以拆分成每工作线程一个工作队列的方式。对于主线程放入工作和工作线程取出任务的竞争问题,可以采取环形队列的方式避免。在解决了锁机制之后,就只剩下条件变量的问题了,条件变量本身即解决条件满足时的线程通信问题,而信号作为一种通信方式,可以代替之,其大体编程范式为:
- sigemptyset (&oldmask);
- sigemptyset (&signal_mask);
- sigaddset (&signal_mask, SIGUSR1);
- rc = pthread_sigmask(SIG_BLOCK, &signal_mask, NULL);
- if (rc != 0) {
- debug(TPOOL_ERROR, "SIG_BLOCK failed");
- return -1;
- }
- ...
- while (!condition) {
- rc = sigwait (&signal_mask, NULL);
- if (rc != 0) {
- debug(TPOOL_ERROR, "sigwait failed");
- return -1;
- }
- }
- rc = pthread_sigmask(SIG_SETMASK, &oldmask, NULL);
- if (rc != 0) {
- debug(TPOOL_ERROR, "SIG_SETMASK failed");
- return -1;
- }
3.无锁化线程池具体实现
在无锁线程池中,区别于常见线程池的地方主要在于信号与条件变量、任务调度算法、增加或减少线程数目后的任务迁移,另外还有一点就是环形队列的实现参考了Linux内核中的kfifo实现。
(1) 信号与条件变量
信号与条件变量的区别主要在于条件变量的唤醒(signal)对于接收线程而言可以忽略,而在未设置信号处理函数的情况下信号的接收会导致接收线程甚至整个程序的终止,因此需要在线程池产生线程之前指定信号处理函数,这样新生的线程会继承这个信号处理函数。多线程中信号的发送主要采用pthread_kill,为避免使用其他信号,本程序中使用了SIGUSR1。
(2) 任务调度算法
常见线程池实现的任务调度主要在操作系统一级通过线程调度实现。考虑到负载均衡,主线程放入任务时应采取合适的任务调度算法将任务放入对应的工作者线程队列,本程序目前已实现Round-Robin和Least-Load算法。Round-Robin即轮询式地分配工作,Least-Load即选择当前具有最少工作的工作者线程放入。
(3) 任务迁移
在线程的动态增加和减少的过程中,同样基于负载均衡的考量,涉及到现有任务的迁移问题。负载均衡算法主要基于平均工作量的思想,即统计当前时刻的总任务数目,均分至每一个线程,求出每个工作者线程应该增加或减少的工作数目,然后从头至尾遍历,需要移出工作的线程与需要移入工作的线程执行任务迁移,相互抵消。最后若还有多出来的工作,再依次分配。迁入工作不存在竞态,因为加入工作始终由主线程完成,而迁出工作则存在竞态,因为在迁出工作的同时工作者线程可能在同时执行任务。所以需要采用原子操作加以修正,其主要思想即预取技术,大致实现为:
- do {
- work = NULL;
- if (thread_queue_len(thread) <= 0) //also atomic
- break;
- tmp = thread->out;
- //prefetch work
- work = &thread->work_queue[queue_offset(tmp)];
- } while (!__sync_bool_compare_and_swap(&thread->out, tmp, tmp + 1));
- if (work)
- // do something
在线程的动态减少后,原先线程上未能执行完的任务只需要由主线程再次根据任务调度算法重新分配至其他存活的工作者线程队列中即可,不存在上述问题,当然,此时可以同时执行负载均衡算法加以优化。
(4) 环形队列
源码中环形队列实现主要参考了Linux内核中kfifo的实现,如下图所示:
队列长度为2的整次幂,out和in下标一直递增至越界后回转,其类型为unsigned int,即out指针一直追赶in指针,out和in映射至FiFo的对应下标处,其间的元素即为队列元素。
以上主要是一些方案性的说明,至于具体细节的实现有兴趣的读者可以参考https://github.com/xhjcehust/LFTPool,有问题欢迎随时联系讨论.
(转)高效线程池之无锁化实现(Linux C)的更多相关文章
- 高效线程池之无锁化实现(Linux C)
from:http://blog.csdn.net/xhjcehust/article/details/45844901 笔者之前练手写过一个小的线程池版本(已上传至https://github.co ...
- java并发包&线程池原理分析&锁的深度化
java并发包&线程池原理分析&锁的深度化 并发包 同步容器类 Vector与ArrayList区别 1.ArrayList是最常用的List实现类,内部是通过数组实现的, ...
- java无锁化编程一:目录
假设我们用netty做服务,当接受到网络传输的码流,我们通过某种手段将这种传输数据解析成了熟悉的pojo,那这些pojo该如何进一步处理? 比如游戏中的抢购.场景业务等,对处理那种高并发的业务场景,如 ...
- Java CAS同步机制 原理详解(为什么并发环境下的COUNT自增操作不安全): Atomic原子类底层用的不是传统意义的锁机制,而是无锁化的CAS机制,通过CAS机制保证多线程修改一个数值的安全性。
精彩理解: https://www.jianshu.com/p/21be831e851e ; https://blog.csdn.net/heyutao007/article/details/19 ...
- Netty源码学习系列之1-netty的串行无锁化
前言 最近趁着跟老东家提离职之后.到新公司报道之前的这段空闲时期,着力研究了一番netty框架,对其有了一些浅薄的认识,后续的几篇文章会以netty为主,将近期所学记录一二,也争取能帮未对netty有 ...
- 高效线程池(threadpool)的实现
高效线程池(threadpool)的实现 Nodejs编程是全异步的,这就意味着我们不必每次都阻塞等待该次操作的结果,而事件完成(就绪)时会主动回调通知我们.在网络编程中,一般都是基于Reactor线 ...
- linux无锁化编程--__sync_fetch_and_add系列原子操作函数
linux支持的哪些操作是具有原子特性的?知道这些东西是理解和设计无锁化编程算法的基础. 下面的东西整理自网络.先感谢大家的分享! __sync_fetch_and_add系列的命令,发现这个系列命令 ...
- 【学习】005 线程池原理分析&锁的深度化
线程池 什么是线程池 Java中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序 都可以使用线程池.在开发过程中,合理地使用线程池能够带来3个好处. 第一:降低资源消耗.通过重复 ...
- Nodejs事件引擎libuv源码剖析之:高效线程池(threadpool)的实现
声明:本文为原创博文,转载请注明出处. Nodejs编程是全异步的,这就意味着我们不必每次都阻塞等待该次操作的结果,而事件完成(就绪)时会主动回调通知我们.在网络编程中,一般都是基于Reactor线程 ...
随机推荐
- IdHTTPServer开发https服务器
IdHTTPServer开发https服务器 该篇经验同样适用于DATASNAP和UNIGUI,因为它们都基于INDY10. 1)需要TIdServerIOHandlerSSLOpenSSL控件 2) ...
- (转载)RNA表观遗传学开创者何川
何川,RNA表观遗传学开创者.早年毕业于中国科技大学,2000年获麻省理工学院博士学位,2000到2002年在哈佛大学做博士后研究,2002年至今执教芝加哥大学化学系, 是芝加哥大学生物物理动态研究所 ...
- Oracle数据库启动报错,找不到数据文件(ORA-01157和ORA-01110)
数据库报了ORA-01157和ORA-01110错误,提示找不到一个数据文件. 1.启动数据库报错 在启动数据库过程中,报了ORA-01157和ORA-01110错误,提示找不到数据文件. SQL&g ...
- AndoridSQLite数据库开发基础教程(5)
AndoridSQLite数据库开发基础教程(5) 创建SQLite数据库 使用SQLiteManager创建数据库的操作步骤如下: (1)双击SQLiteManager工具,弹出SQliteMana ...
- [转]eclipse常用设置
原文:https://www.cnblogs.com/wangmingshun/p/5675857.html Eclipse重新设置workspace Window—>Preferences—& ...
- Spark累加器(Accumulator)
一.累加器简介 在Spark中如果想在Task计算的时候统计某些事件的数量,使用filter/reduce也可以,但是使用累加器是一种更方便的方式,累加器一个比较经典的应用场景是用来在Spark St ...
- WPF-支持异步操作的ObservableCollection-AsyncObservableCollection
在进行WPF开发过程中,需要从一个新的线程中操作ObservableCollection,结果程序抛出一个NotSupportedException的错误 public class AsyncObse ...
- 零基础学Python-第一章 :Python介绍和安装-03.Python的安装
官方版本的python下载以及安装方法,以及pycharm的安装和打开. 社区版就可以完全支持我们的需求了. 点击左侧的图片到右边. 在命令行输入python3 exit() 退出命令行的编辑器. p ...
- 【Mybatis】MyBatis之配置多数据源(十)
在做项目的过程中,有时候一个数据源是不够,那么就需要配置多个数据源.本例介绍mybatis多数据源配置 前言 一般项目单数据源,使用流程如下: 单个数据源绑定给sessionFactory,再在Dao ...
- 关于STM32F405的GPIO中断问题
1. 下面的图,应该是多个引脚中断挂在同一个中断号上面,也就是PA0和PB0同时挂在一个中断源上面,那么就是说只能同时使用其中一个 寄存器的配置,确实只能有一个使用