一、应用的线程池
在前面写了一个入门的线程,但实际应用时,一般很少会有这么简单的线程池。在本篇将会开始写一个较复杂的面向应用的线程池,当然也不是说这个线程池就多么强大,只是把这种线程池的应用推广到一个可以面向应用的场景。
一般来说,在实际应用的线程池要可以对线程中的线程进行控制并进行各种任务的分发和执行等操作,从这次起将在这些需求指导下写一个初步的面向应用的线程,以后在这个线程池的基础上不断进行完善。
二、重点分析
一个面向应用的线程池,需要处理的几个重问题:
1、保证线程池的弹性扩展,也就是可伸缩。
2、动态适配硬件和相关环境。
3、任务的分配要保证安全和效率。
4、任务之间的交互与同步控制。
5、线程的安全退出(含资源清理等)。
在后面的这个系列文章中,会逐渐从一个基础的线程池版本不断的扩展,最终实现一个相对完整的线程池。
三、主要模块
1、任务处理模块
任务处理模块在前面已经说明的非常清晰,但没有单独进行管理,在实际应用中,一般会单独成为一个模块进行线程池各种任务的管控。
2、数据管理模块(数据队列)
这一块的核心其实就是一个HA/HS中常用的,需要一个同步的数据处理机制,一般来说,任务更多的落在数据的处理上。不管是一个线程(或核心)单独成立自己的数据队列还是共享一个数据队列,最终总不可避免的会有线程间数据的交互,这个数据队列就需要了。
3、线程管理模块
前面也已经分析过,就是一个线程创建、维护、回收的一个生命周期管理以及线程在使用中的动态调度等等。
4、内存管理模块
数据管理,经常就会遇到内存分配的问题,这里就很可能会有一个内存管理的模块,用来进行内存的动态创建、使用和回收。当然,可能这里就会创建一个内存池,这需要根据实际情况来决定。
5、接口模块
接口模块是体现线程池易用性的一个重要表现。包括数据和任务的插入,线程处理完成的数据的回传或者异步处理等等。
四、代码
老样子,代码先拿出来晒晒:
//common.h
#include <functional>
using CallBackMsg = std::function<void(int*,int)> ;
using Task = std::function<void(int)>;
//taskqueue.h
#pragma once
#include <queue>
#include <tuple>
#include <mutex>
template<typename T>
class TaskQueue final
{
public:
TaskQueue() = default;
~TaskQueue() = default;
public:
void Push(T t)
{
std::unique_lock lock(this->mu_);
this->queue_.emplace(t);
}
std::tuple<bool,T> Get()
{
std::unique_lock lock(this->mu_);
if (!this->queue_.empty())
{
T t = this->queue_.front();
this->queue_.pop();
return {true,t};
}
else
{
return { false,T{} };
}
}
void Clear()
{
std::queue<T> tmp;
std::swap(this->queue_, tmp);
}
private:
std::queue<T> queue_;
std::mutex mu_;
};
//threadworker.h .cpp
#pragma once
#include <atomic>
#include <thread>
#include <optional>
#include "common.h"
class ThreadWorker
{
public:
ThreadWorker();
~ThreadWorker()=default;
public:
void InitThread(bool initStatus,CallBackMsg cb);
void Run();
void Join();
std::optional<std::thread::id> GetCurThreadID()
{
return this->curThreadId_;
}
private:
std::shared_ptr<std::thread> pWorkerThread_ = nullptr;
std::atomic<bool> status_ = false;
std::thread::id curThreadId_;
CallBackMsg cbm_;
};
#include "ThreadWorker.h"
#include <iostream>
#include "ThreadPool.h"
ThreadWorker::ThreadWorker() {}
void ThreadWorker::InitThread(bool initStatus,CallBackMsg cb)
{
this->cbm_ = cb;
this->pWorkerThread_ = std::make_shared<std::thread>(&ThreadWorker::Run,this);
if (nullptr != this->pWorkerThread_)
{
this->curThreadId_ = this->pWorkerThread_->get_id();
}
}
void ThreadWorker::Run()
{
int data[10] = {0};
auto tPool = ThreadPool::Get();
while (!status_)
{
auto [bRet, t] = tPool->GetTask();
if (bRet)
{
t(10);
data[0] = 10;
data[1] = 11;
data[2] = 12;
cbm_(data,3);
}
else
{
std::this_thread::sleep_for(std::chrono::milliseconds(100));
if (this->cbm_ != nullptr)
{
cbm_(data,0);
}
}
std::this_thread::yield();
}
}
void ThreadWorker::Join()
{
if (this->pWorkerThread_ != nullptr && this->pWorkerThread_->joinable())
{
this->pWorkerThread_->join();
}
}
//threadpool.h .cpp
#pragma once
#include <memory>
#include <vector>
#include "common.h"
#include <atomic>
#include "TaskQueue.h"
class ThreadWorker;
class ThreadPool
{
public:
explicit ThreadPool();
~ThreadPool();
public:
void InitThreadPool(int threadCount, bool initThreadStatus,CallBackMsg cb);
void AddTask(Task t);
void Destory();
std::tuple<bool, Task> GetTask();
public:
static std::shared_ptr<ThreadPool> Get();
private:
std::vector<std::shared_ptr<ThreadWorker>> pVecThreadWorker_;
CallBackMsg funcCallBack_ = nullptr;
std::atomic<int> curId_ = 0;
TaskQueue<Task> taskQueue_;
};
#include "ThreadPool.h"
#include "ThreadWorker.h"
ThreadPool::ThreadPool() {}
ThreadPool::~ThreadPool() {}
void ThreadPool::InitThreadPool(int threadCount, bool initThreadStatus,CallBackMsg cb)
{
this->funcCallBack_ = cb;
for (int num = 0; num < threadCount; num++)
{
auto workerThread = std::make_shared<ThreadWorker>();
workerThread->InitThread(initThreadStatus,cb);
this->pVecThreadWorker_.emplace_back(workerThread);
}
}
void ThreadPool::AddTask(Task t) {
this->taskQueue_.Push(t);
}
void ThreadPool::Destory() {
for (auto& au : this->pVecThreadWorker_) {
au->Join();
}
}
std::tuple<bool,Task> ThreadPool::GetTask()
{
return this->taskQueue_.Get();
}
std::shared_ptr<ThreadPool> ThreadPool::Get()
{
static auto threadPool = std::make_shared<ThreadPool>();
return threadPool;
}
//main.cpp
#include <iostream>
#include "ThreadPool.h"
void DisplayResult(int* buf,int size)
{
if (size < 1)
{
std::cerr << "not task run!" << std::endl;
return;
}
std::cout << "run result:" << std::endl;
for (int num = 0; num < size; num++)
{
std::cout << "first data:"<<buf[num] << std::endl;
}
}
void AppTask(int d)
{
if (d > 0)
{
std::cout << "cur value is:" << d << std::endl;
return;
}
std::cout << "err:nothing to do!" << std::endl;
}
int main()
{
auto pTp = ThreadPool::Get();
pTp->InitThreadPool(6, false,DisplayResult);
pTp->AddTask(AppTask);
char a;
std::cin >> a;
}
上面的代码很粗糙,很多的功能都需要完善,仔细分析可以看到,其实它就是从简单的线程池的框架改造过来的。这样做的目的,就是让初学者能更好的知道代码演进的过程,而不是直接上来一个非常全面的线程池,不知如何下手。好的线程池代码在网上非常多,大家想用可以去Github上搜一搜。
这样先起一个框架,然后再按照上面的主要模块在后续的过程中不断的完善,就可以基本掌握一个线程编写的过程。相对于简单线程池,这里首先是接口处理增加了对外的任务数据(数据处理)完成的回调(当然,此处也可以使用消息或者回写共享数据队列等的操作),这里不使用std::future结果处理更符合线程池的异步发送通知和结果的应用场景;其次增加一个任务处理的模板队列供线程消费,当然,基础的任务管理和线程管理这里也有,只是相当的简陋。
此处对任务的包装也比较简单,会在后续不断的进行封装处理。还有消费的数据队列、线程的管理和扩展等等,都需要不断的进行完善。另外,此处为了容易理解,暂时先忽略前面提到的使用条件变量来触发启动线程,而是直接启动线程,这些其实都可以归到线程的管理中去。
四、总结
写线程其实是一个很考验经验的活儿,不光要掌握理论,对实际中使用线程遇到的各种问题,往往理论不会解释这么细。但是,如果在解决问题的过程中,就会发现,其实这些与理论是完全相符合的。
而设计一个好的线程池需要不断的从理论上指导完善并且在实际场景不断打磨。还是老话,不可能有一个放之四海而皆准的线程池,但肯定有一个在本公司用起来很不错的线程池。
庄子在《人间世》中说:“其作始也简,其将毕也必巨!”与诸君共勉!