线程池的自我修养
Posted 雀观代码
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了线程池的自我修养相关的知识,希望对你有一定的参考价值。
最近重构行情服务端的框架,其中有一部分就是重写mysql线程池,线程池是一个很独立的东西,今天就拿出来给大家分享, 怎样设计一个线程池, 以及我是怎么做的.
为什么要使用线程池
常见的线程池使用场景分为两种:
这个很好理解, 当程序需要大量计算, 单核CPU跑到100%, 这个时候可以将计算任务分解, 分多个线程计算, 如果我们有4核, 那这个时候我们可以跑到400%, 理想情况下, 可以节省3倍的时间. 当然这个不是绝对的, 具体情况要具体分析. 总而言之, 是为了让程序充分打满CPU.
如果这个是web程序, 异步绝对是提高并发的神器. 在我们的C++服务器中, 也会有大量的阻塞任务, 可能是读取mysql, 可能是读取mongodb, 或者任意需要同步等待完成的事情, 那么在等待的时候, 我们的工作线程是完全没法做别的工作的, 这个时候我们就把等待的过程, 变成一个任务, 让线程池去做, 主线程继续处理别的工作, 等线程池完成之后, 再接管任务, 继续往下面执行.
这是两种完全不同的工作内容, 看上去都是线程池, 需要注意的细节, 是完全不一样的, 比如开启的线程数量, 大量计算的时候, 我们开的线程, 尽量是小于CPU数量的, mysql访问的时候, 线程数一定是不能高于mysql的并发数的. 这种细节很多, 不同的情景情况不一样, 不能一概而就.
线程池的自我修养
今天我要给大家分享的线程池, 抛开任务的细节, 主要讲我们应该怎样去设计一个线程池.
不管任务多么复杂, 最终都在这个模型上. 重点可以分为下面几个:
线程间的通信
调度线程的设计
任务的抽象
每个点的设计, 不同的人有不同的方法, 向大家分享我的方法, 主要针对的是mysql线程池的设计, 仅供大家参考.
线程间通信有很多种方法, 可能是信号, 可能是管道, 可能是套接字, 我比较喜欢更高级的封装zmq. 不管怎样的通信方式, 我们需要保证下面两点:
全异步
不管是主工作线程与调度线程之间, 还是调度线程与线程池线程之间, 一定是异步完成, 绝对不允许同步, 任何地方有同步逻辑, 将成为整个线程池的瓶颈.
一问一答
一个请求, 只能返回一次, 绝对不能一问多答, 更不能只问不答. 线程池要向主工作线程保证, 过来的请求, 一定会返回, 并且有且只有一次返回. 同时我建议, 如果线程池内部发生执行异常, 不要做二次尝试, 直接将异常标记返回.
通信模块的设计, 要保证简单高效, 给外面暴露的接口简单到只有接收任务和发送结果两个接口, 过多冗余的设计, 只是无畏的增加了复杂度.
调度线程需要关注的也是两点:
外部消息队列
这部分我也喜欢交给zmq去做, 有任何消息的时候直接回调, 这里我将外部主线程消息与线程池消息都放在一个消息队列, 既符合先进先出的模式, 也符合单线程同步执行的逻辑.
任务队列
当过来的任务超过线程池真实并发数量的时候, 我们会将任务缓存在队列, 然后当工作线程执行完任务的时候,或者有新的任务过来的时候, 我们都会去检查是否有空余的工作线程, 然后将任务分配给工作线程.
将所有的工作抽象成通用的任务, 得益于C++的类型转换, 我们可以将所有的入参, 和出参都打包成一个void*, 然后将具体执行任务的过程, 使用一个静态函数, 这样打包一个通用的工作任务.
/**
* @brief 给db层发送的参数
*/
struct DBParam
{
DBParam():
m_type(fund_begin),
m_seq(0)
{}
//! 需要执行的sql
std::string m_str_sql;
//! db的类型
db_res_type m_type;
//! 请求的seq
uint64_t m_seq;
};
/**
* @brief 从db返回的数据
*/
class ResFund:
public ResBase
{
public:
ResFund(){}
//! 基础数据集合
std::vector
m_vec_funds; };
//! 交给各个服务的正真执行sql的回调函数
typedef void (*DBQueryHandler)(MYSQL* con, void* param, void* res)
class DbMessage:
public MessageBase{
public:
/**
* @brief 构造函数
*/
DbMessage();
/**
* @brief 析构函数
*/
virtual ~DbMessage();
//! 需要执行的参数
void* m_params;
//! 执行之后, 产生的结果信息
void* m_msg;
//! 执行mysql的回调
DBQueryHandler m_handle_fun;
};
上面的代码删除了一些敏感的信息, 将主体拿出来, 大致表示我是怎么打包一个任务的. 事实上不管线程池做得多么的好, 业务千变万化, 我们很难满足的, 而我们这个任务的封装最重要的就是把业务封装到任务里面, 我们通过一个DBQueryHandler的回调函数, 主工作线程将自己的业务写到回调里面, 交由工作线程完成, 进而实现业务的千变万化.
看过大多线程池的实现, 很多人都喜欢用锁, 比如消息队列, 任务队列, 用各种锁来竞争, 进而实现任务的分发, 不敢说这个性能怎么样, 但是一旦扯上锁, 整个代码复杂度就上去了, 一处用锁, 到处加锁. 这个线程池的设计是完全没有任何锁的, 单线程内部完全是消息驱动, 线程间消息投递, 简单高效.
简单,简单,再简单
线程池的设计见仁见智, 不同的设计可能基于不同的需求, 没有银弹. 但是一定要把接口设计得简单, 不要有酷炫吊炸天的功能, 良好的文档, 对使用者友好, 一眼就能看懂的接口, 才是我们要追求的, 一句话, 简单,简单,再简单.
欢迎大家订阅雀观代码, 我将给你讲述, 中小企业程序员, 淘金路上的故事.
以上是关于线程池的自我修养的主要内容,如果未能解决你的问题,请参考以下文章
『Java练习生的自我修养』java-se进阶³ • 线程的等待与唤醒