线程池并行化重构

作者 YYGCui 日期 2021-12-03
Categories: 技术积累 Tags: C/C++
线程池并行化重构

最近业务新特性开发,由原来的单个action变成了多个action,最后取一个最优action。由于action变多,运行时间也相应多了几倍,action之间是相互独立,互不影响的,那么就想通过并行化的方法提升下性能,这个时候自然而然就想到了线程池。本文记录一下此次并行化重构业务代码时遇到的一些有意思的点。

线程池

模块中本来就包含线程框架,可以适当修改支持线程池,这里没啥大问题。业务代码action处理函数的改造主要在于函数输出,由原来的串行执行使用的成员变量改为变量数组,对应于线程池的个数。原有的依次比较改成最后遍历数组比较取最优。这块重构问题主要是内存问题,有些输出变量是对输入变量的局部更新,但是该变量是很大的数据结构,直接变成数组,造成内存使用增加很多,当前通过增加输出数据结构解决,这里的坑和多线程无关,另外单独介绍。

单例

这块业务代码有一个特色,就是单例使用特别多,且单例不仅仅是状态资源管理类,同时还是业务功能类。这里单例使用的是否合理,软件架构设计的是否合理不做深入讨论。当前只讨论怎么重构支持多线程。

重构为类成员变量

首先想到的方法是,既然不是全局的状态资源管理类,那么改成类成员比较合理,这样多线程处理时可以像上面的函数输出,改成数组就能满足需求。但通过阅读代码发现单例互相调用,随处调用等等,这种方法难以实现。

重构为多单例

这种方法比较省事,把原来的静态实例变成静态实例数组,获取单例接口通过增加参数来区分获取的是哪一个实例,从而和线程对应起来。代码如下:

static const MyClass& GetInstance(uint8_t threadIndex) {
static MyClass instance[THREAD_POOL_SIZE];
return instance[threadIndex];
}

多线程处理没有问题了,现在的问题变成如何获取threadIndex

一个方法是给线程池的各线程命名,建立线程名称与threadIndex的map关系,这样在线程内可以通过系统接口获取线程名称,再通过map得到threadIndex。这种方法使用起来比较方便,但是有一个问题,单例特别多且高频调用,频繁调用系统接口比较影响性能。

另一种方法是通过参数传递进去,根据业务代码特点,每个类都有一个初始化配置接口InitConfig,这个接口在系统启动时执行一次。那么可以给每个类增加一个成员变量threadIndex_,在线程入口函数类的初始化时,遍历入口多单例并把index传入。在把action处理函数提交给线程池处理时,增加index参数,这样每个类在调用单例时都是通过传入的index调用的。代码如下:

MyClass::InitConfig(uint8_t index) {
threadIndex_ = index;
...
}

# 入口初始化时对多单例的初始化
for (uint8_t i = 0; i < THREAD_POOL_SIZE) {
MyClass::GetInstance(i).InitConfig(i);
}

# MyClass类中调用其他单例时
OtherClass::GetInstance(threadIndex_).HandleFunc(...)

这种方法相对比较可行,就是修改非常多,每一个调用单例且会更新的类都要修改。

单例修改完成后,使用线程池调式时发现还是会coredump,不用想肯定是多线程同时读写同一份数据造成的。通过调用栈看到的函数调用关系,看不出来具体问题点在哪里,虽然每次调用栈都不太一样,但是出问题的类及函数是相同的,且看到是同一个结构里的容器拷贝出了问题。通过增加打印及阅读代码,发现了另一个坑…

static变量

某些类成员函数中,局部变量定义时使用了static,但是没有使用历史值,猜测是想为了避免频繁构造析构该变量。但是C++中类中出现的static变量,不管是类成员变量还是函数局部变量,是独立于类实例的,所有该类的实例共享一份static变量。这就很清楚了,多线程调用时,这些线程共享了这一个变量,同时读写时出现踩内存问题。

回到局部变量使用static上,在面向过程实现中,staitc局部变量可以保证该变量的作用域只在函数内,避免使用全局变量造成全局可见,起到很好的封装作用。但是在面向对象实现中,这种隐藏在层层逻辑下的static局部变量是有很大的隐患的,因为它不是类的成员变量,却又所有实例共享一份。定义成普通类成员变量一样可以避免频繁构造析构,且重用该变量的值。

通过以上修改,终于调通了并行化~