需求分析
任何一个对性能有所要求的程序中,多线程是个绕不过去的优化手段。直接使用多线程并不能完全的发挥其作用。
假设某个算法的pipeline有N个模块M1,M2,M3,M4…MN。
首先若直接使用多线程技术,会导致我们在每个模块中都要重复一遍创建线程的操作,从而影响总体性能。
其次在这种重复创建线程的情况下也有可能会导致线程进入睡眠状态,而无法快速的利用CPU资源。所以由线程池统一的安排线程创建和调度就显得很有必要。
再考虑更复杂点的情况。M1到M5是需要多线程来加速,M6到M10不需要,随后M11到16又需要多线程加速,等等。显然在连续需要多线程加速的模块中,我们希望线程一直保持在使用状态,而不要进入休眠状态,避免反复唤醒带来的性能损失。而在不需要多线程的连续模块中,我们希望多线程进入休眠,让出资源给系统的其他进程,同时减小整个系统的功耗。
设计目标
综合上一节,可以给出线程池的设计目标。
- 统一的线程创建,销毁
- 线程数目在每次任务中可根据要求调整(比如模块1用4个线程,模块2用8个)
- 灵活控制线程在等待的时候是进入忙等还是睡眠状态。
- 不浪费CPU资源,主线程也参与使用
接口预览
class ThreadArg
{
public:
AGENT_FUN imp;
std::condition_variable condition_todo;
std::vector<void*> argv_fun;
std::vector<bool> isActive;
std::vector<bool> isUnDone;
std::vector<bool> isBusyWait;
std::mutex mutex_task;
spin_mutex mutex_spin;
bool isQuit;
int threadUsed;
};
class ThreadPool {
public:
void InitPool();// 初始化线程池
void Start(AGENT_FUN imp, std::vector<void*> &argv_fun);// 开启多线程任务,imp为线程需要实现的代码段,argv_fun为参数列表,有多少个元素代表需要启动多少线程
void Wait();// 任务同步点
void purse(); // 激活的线程进入睡眠等待
void UnInitPool();// 反初始化线程池
private:
ThreadArg _argv;
std::vector<std::thread> _vThreadPool;
const int _MAX_THREAD_NUM = 16;
};
关键实现摘要
初始化
我们把所有的线程设定为睡眠等待状态,并把激活状态设置成否。此外给每个线程设定个ID方便对其标志位进行管理。
void ThreadPool::InitPool()
{
int MAX_NUM = _MAX_THREAD_NUM;
for (int i = 0; i < MAX_NUM; i++)
{
_argv.isUnDone.push_back(true);
_argv.argv_fun.push_back(NULL);
_argv.isBusyWait.push_back(false);
_argv.isActive.push_back(false);
}
_argv.isQuit = false;
for (int i = 1; i < MAX_NUM; i++)
{
_vThreadPool.emplace_back(process_thread,(void*)(&_argv), i);// 传入参数和ID
}
return;
}
线程主体
主体为一个死循环,直到反初始化时才退出循环。通过标志位来决定其状态,分别为睡眠等、忙等、退出和运行目标任务。
static void process_thread(void *arg_src,int id)
{
ThreadArg *arg = static_cast<ThreadArg*>(arg_src);
while (1)
{
if (!arg->isBusyWait[id])
{
std::unique_lock<std::mutex> lk(arg->mutex_task);
printf("[%d] Wait.\n", id);
while (!(arg->isActive[id]&&arg->isUnDone[id])) // active and this thread don't finish it's work
arg->condition_todo.wait(lk);
lk.unlock();
}
else
{
printf("[%d] Busy Wait.\n", id);
while (1)
{
std::lock_guard<spin_mutex> lk(arg->mutex_spin);
if ((arg->isActive[id]&&arg->isUnDone[id]) || !arg->isBusyWait[id])
{
break;
}
}
}
if (arg->isQuit)
{
printf("[%d] Quit.\n",id);
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
break;
}
if (arg->isActive[id])
{
printf("[%d] Do Work\n", id);
// do the work
arg->imp(arg->argv_fun[id]);
std::lock_guard<std::mutex> lk(arg->mutex_task);
arg->isUnDone[id] = false;
}
}
return ;
}
测试代码
测试代码中启动了两次任务,每次任务都是需要3个线程(主线程为0号线程)。第一次任务运行结束后,这次任务用到的线程为忙等状态,期望后续马上有其他任务被设置。第二次任务运行结束后,设定为睡眠等待。期望后续短时间内无其他任务,让出CPU资源给其他进程/线程。线程池中一共16个线程,未用到的线程都进入休眠状态。
int theadPool()
{
int k[4] = { 0,1,2,4 };
std::vector<void*> params;
params.push_back(k);
params.push_back(k + 1);
params.push_back(k + 2);
ThreadPool pool;
pool.InitPool();
pool.Start([](void *param) { auto *pInt = static_cast<int*>(param); printf("first imp %d\n",*pInt); }, params);
pool.Wait();
//while (1){} // uncomment tiis line will see,CPU is busy
k[0] = 111;
k[1] = 111;
k[2] = 222;
pool.Start([](void *param) { auto *pInt = static_cast<int*>(param); printf("Second imp %d\n", *pInt); }, params);
pool.purse();
pool.Wait();
//while (1){} // uncomment tiis line will see,CPU is sleep
std::this_thread::sleep_for(std::chrono::milliseconds(2000));
pool.UnInitPool();
return 0;
}
下图是运行结果。
下图为忙等状态(注释掉第一个while(1))下的CPU使用率。测试机器为4核CPU,本例3个线程忙等大概会有80%CPU使用率。
下图为睡眠等待状态(注释掉第二个while(1))下的CPU使用率。因为只有主线程在while(1),所以大概25%的CPU使用率。
完整代码
https://github.com/duzeyan/cppMutilThreadPool
声明
本程序未经过生产环境测试,仅供学习交流。如有问题欢迎留言私信讨论。