本篇的代码来自于 一个Windows下线程池的实现(c++),同时,由于我的开发环境是clion+cmake,不是用的vs,所以也贴一下源码地址:这里
原文中工作原理图已经很明白的介绍了这个线程池的实现架构,这里为了我学习的需要,从代码角度分析这个小例子。
代码结构方面,我将整个demo分为了两块,线程管理包,以及 任务包。 二者是以动态库的形式进行调用,也是为了熟悉cmake的使用。
thread包中,核心类ThreadPool的api如下,以代码块的形式进行解读(threadPool此处实际上更像一个命名空间):
typedef int(*TaskFun) (PVOID param);
typedef void(*TaskCallbackFun) (int result);
首先,定义了两个 函数指针,这是以前没有接触过的新鲜玩意,没想到函数还能当作变量一样使用,这是大大的拓宽了我的眼界,同时让我想到了java8的 新特性里面就有传递参数的好像。 这两个函数的作用: 一个为 线程的任务函数,它的参数为无状态指针,返回值为 int; 另一个为 任务回调函数,以状态码为参数,没有返回值。
//threadpool的线程内部类
class Thread{
public:
Thread(ThreadPool *threadPool);
~Thread();
BOOL isBusy(); //是否有任务在执行
void executeTask(TaskFun task,PVOID param,TaskCallbackFun taskCallback); //执行任务
private:
ThreadPool *threadPool; //所属线程池
BOOL busy; //是否有任务在执行
BOOL exit;//是否退出
HANDLE thread; //线程句柄
TaskFun task; //要执行的任务
PVOID param; //任务参数
TaskCallbackFun taskCb; //回调任务
static unsigned int __stdcall ThreadProc(PVOID pM); //线程函数
};
这里要注意的是,此处的线程为 系统线程的抽象,包括线程状态的维护等等,这个抽象的原因在于,我们可以复用。因为原生线程它的某种状态与实际状态时一致的,这就说明,它相应状态的改变,必然会引起相应的操作,当线程状态变更频繁时,效率会打折扣。
//IOCP的通知种类
enum WAIT_OPERATION_TYPE{
GET_TASK, //获取到任务
EXIT //退出
};
这是定义在threadpool的内部枚举,用于线程间通信的消息类型。
//threadpool 的内部类,待执行的任务
class WaitTask{
public:
WaitTask(TaskFun task,PVOID param,TaskCallbackFun taskCb,BOOL bLong);
~WaitTask();
TaskFun task;//要执行的任务
PVOID param;//任务参数
TaskCallbackFun taskCb;// 回调的任务
BOOL bLong; //是否时长任务
};
这个waitTask类比到java中,就是个典型的PO类,属性集。
//线程临界区锁
class CriticalSectionLock{
private:
CRITICAL_SECTION cs;//临界区
public:
CriticalSectionLock();
~CriticalSectionLock();
void Lock();
void UnLock();
};
线程临近区,用于多线程环境下,保证线程安全。
public:
ThreadPool(size_t minNumOfThread =2,size_t maxNumOfThread =10);
~ThreadPool();
BOOL QueueTaskItem(TaskFun task,PVOID param,TaskCallbackFun taskCb = NULL,BOOL longFun = FALSE); //任务入队
private:
size_t getCurNumOfThread(); //获取线程池中的当前线程数
size_t getMaxNumOfThread(); //获取线程池中的最大线程数
void setMaxNumOfThread(size_t size); //设置线程池中的最大线程数
size_t getMinNumOfThread(); //获取线程池中的最小线程数
void setMinNumOfThread(size_t size); //设置线程池中的最小线程数
size_t getIdleThreadNum(); //或许线程池中的空闲线程数
size_t getBusyThreadNum(); //获取线程池中的运行线程数
void createIdleThread(size_t size); //创建空闲线程
void deleteIdleThread(size_t size); //删除空闲线程
Thread *getIdleThread(); //或许空闲线程
void moveBusyThreadToIdleList(Thread *busyThread); //忙碌线程加入空闲队列
void moveThreadToBusyList(Thread *thread); //线程加入忙碌列表
void getTaskExecute(); //从任务队列中取任务执行
WaitTask *getTask(); //从任务队列中取任务
CriticalSectionLock idleThreadLock; //空闲线程列表锁
std::list<Thread *> idleThreadList; //空闲线程列表
CriticalSectionLock busyThreadLock; //忙碌线程列表锁
std::list<Thread *> busyThreadList; //忙碌线程列表
CriticalSectionLock waitTaskLock; //任务锁
std::list<WaitTask*> waitTaskList; //任务列表
HANDLE dispatchThread; //分发任务线程
HANDLE stopEvent; //通知线程退出的事件
HANDLE completionPort; //完成端口
size_t maxNumOfThread; //线程池中最大的线程数
size_t minNumOfThread; //线程池中最小的线程数
size_t numOfLongFun; //线程池中时常 线程数
上面则是threadPool的一些核心属性及接口,它的核心有两点,第一,管理线程(注,这个是抽象的线程。); 第二,管理任务。api过完了,接着将关键的实现部分给看一看。
ThreadPool::ThreadPool(size_t minNumOfThread,size_t maxNumOfThread){
if(minNumOfThread<2)
this->minNumOfThread =2;
else
this->minNumOfThread=minNumOfThread;
if(maxNumOfThread<this->minNumOfThread*2)
this->maxNumOfThread = this->minNumOfThread*2;
else
this->maxNumOfThread = maxNumOfThread;
//停止事件
stopEvent = CreateEvent(NULL,TRUE,FALSE,NULL);
//实例化完成端口,分配内存IO
completionPort = CreateIoCompletionPort(INVALID_HANDLE_VALUE,NULL,0,1);
idleThreadList.clear();
createIdleThread(this->minNumOfThread);
busyThreadList.clear();
//实例化分发线程
dispatchThread = (HANDLE) _beginthreadex(0,0,GetTaskThreadProc,this,0,0);
numOfLongFun =0;
}
threadPool在实例化时,会传入最小线程数,和 最大线程数 这两个参数,方法内部会对最小线程数进行限度,不得小于2个。最大线程数低于最小线程数的2倍,则最大线程数为 当前最小线程数的两倍。
然后通过windows的API分配资源,包括 停止事件句柄,完成端口句柄。之后,根据最小线程数,创建空闲线程,并加入空闲线程队列。 同时为了确保安全性,清空活跃线程队列。
最后,开启一个分发任务的线程,这个线程用于获得任务,然后将任务派发给空闲线程。
void ThreadPool::createIdleThread(size_t size) {
idleThreadLock.Lock();
for(size_t i=0;i<size;i++){
idleThreadList.push_back(new Thread(this));
}
idleThreadLock.UnLock();
}
创建线程时,是以线程安全(临界区)的形式,将新实例化的线程加入 空闲线程队列。
ThreadPool::Thread::Thread(ThreadPool *threadPool):busy(FALSE),thread(INVALID_HANDLE_VALUE),task(NULL),taskCb(NULL),exit(FALSE),threadPool(threadPool)
{
thread = (HANDLE)_beginthreadex(0, 0, ThreadProc, this, CREATE_SUSPENDED, 0);
}
而实例化抽象线程的时候,方法内部会 在操作系统中 实际上开启一个线程。 不过有几个注意的点:
1.新开的线程状态是: CREATE_SUSPENDED,意味着该线程被系统分配后,并不会立即参与调度执行,而是挂起在线程队列中。
2.抽象的线程在实例化时,均会反向绑定到一个ThreadPool。由于本程序中,是以内部类的形式设计的,所以它实际上绑定的就是外部的ThreadPool对象。
3.新开的线程,以当前抽象线程为参数,进行传递。
接着我们看下,新开线程的任务是一个怎样的逻辑:
unsigned int ThreadPool::Thread::ThreadProc(PVOID pM) {
Thread *pThread = (Thread*)pM;
while (true){
if(pThread->exit)break; //线程退出
//首次检查
if(pThread->task==NULL&&pThread->taskCb==NULL){
pThread->busy = FALSE;
pThread->threadPool->moveBusyThreadToIdleList(pThread);
SuspendThread(pThread->thread);
continue;
}
//构造方法
int result =pThread->task(pThread->param);
if(pThread->taskCb)pThread->taskCb(result);
WaitTask *waitTask = pThread->threadPool->getTask();
//取到了任务就继续执行
if(waitTask!=NULL){
pThread->task = waitTask->task;
pThread->taskCb = waitTask->taskCb;
delete waitTask;
continue;
} else{
pThread->task =NULL;
pThread->param = NULL;
pThread->taskCb =NULL;
pThread->busy = FALSE;
pThread->threadPool->moveBusyThreadToIdleList(pThread);
SuspendThread(pThread->thread);
}
}
return 0;
}
从代码中,我们可以知晓这样的一些信息:
1.当前线程的执行逻辑首先是一个 无限循环的路径;
2.每条路径中,会根据传入的抽象线程的状态,做出相应的动作。
如抽象退出状态被激活,则退出该无限循环,继而这个线程也会退出。
然后判断 传入的抽象线程是否有 任务以及 任务对调函数,如二者均为空,则修改当前的抽象线程的状态为空闲,并从活跃线程队列 移至 空闲线程队列。 然后挂起当前抽象线程的 实际线程句柄(而不是回收,注意)。
以上检查均通过后,通过前面定义的 函数指针,后加括号,进行函数执行。根据执行的任务状态码,有任务回调函数则执行任务回调,同时我也认为,这是整个设计流程之所以能够运行的最根本所在。 这是另一个颠覆我以前认知的地方,在以前学习js的时候,也学过自执行函数,闭包等等,也有介绍收 "()" 为函数触发符号,本以为那个js的个性,没想到c系语言也是这样。后面才了解到 它们本是同源,js也是c系语言风格。
执行完后,通过当前绑定的线程池继续获取任务,获取到了任务则继续执行任务。没获取到任务,则修改相应状态和属性,并挂起当前抽象线程的线程句柄。 同时开始下一轮路径循环。
从线程的角度来理解,本来一个线程要执行,它的代码应该是固定的,要么异常退出,要么代码执行完毕而结束。 而这里通过对线程的抽象,达到了动态执行分散代码块的效果。 这种类似的动态增删代码块的功能,我们也不是没有接触过,如 观察者模式,责任链模式,java中的动态代理也即所谓的 AOP,都可以完成类似的功能。 只不过,从抽象程度看,后者是在代码内部的抽象,而线程池是对线程的抽象。 从目的来看,后者是为了扩展伸缩,而线程池 是为了复用。
看完了这个工作线程,我们再来看看调度线程,因为从程序使用者角度,对 线程与 任务 二者的分别,是无感知的,例如本例的测试的代码:
int main(){
ThreadPool threadPool(2,6);
for(size_t i=0;i<10;i++){
threadPool.QueueTaskItem(Task::task1,NULL,TaskCallBack::taskCallback1);
}
threadPool.QueueTaskItem(Task::task1,NULL,TaskCallBack::taskCallback1,TRUE);
//system("pause");
return 0;
}
在线程池开始调度程序的时候,与工作线程有区别,那就是它是新开了线程,便立即参与调度执行,它的工作函数如下:
//从任务队列取任务的线程函数
unsigned ThreadPool::GetTaskThreadProc(PVOID pM){
ThreadPool* threadPool = (ThreadPool*)pM;
BOOL bRet = FALSE;
DWORD dwBytes = 0;
WAIT_OPERATION_TYPE opType;
OVERLAPPED* ol;
while(WAIT_OBJECT_0!=WaitForSingleObject(threadPool->stopEvent,0)){
BOOL bRet = GetQueuedCompletionStatus(threadPool->completionPort,&dwBytes,(PULONG_PTR)&opType,&ol,INFINITE);
if(EXIT ==opType){
break;
} else if(GET_TASK ==opType){
threadPool->getTaskExecute();
}
}
return 0;
}
注意到,该线程以接收到 stopEvent信号为结束,在信号量没有被激活的情况下,它可以类似的比作无限循环。 同时,在这个循环结构中,存在一个阻塞语句,该语句通过 完成端口 接收相应的信息。(这个完成端口在windows用处挺大,后文也将进一步研究其与socket编程的结合使用。)。 当然了,接收到了信息,自然就是根据信息类别去做出相应的处理。
最后,再看一下 getTastExecute如何与 工作线程相互联系起来的。
void ThreadPool::getTaskExecute() {
Thread *thread = NULL;
WaitTask *waitTask = NULL;
waitTask = getTask();
if(waitTask ==NULL){
return;
}
//如果是时常任务
if(waitTask->bLong){
if(idleThreadList.size()>minNumOfThread){
thread = getIdleThread();
}else{
thread = new Thread(this);
InterlockedIncrement(&numOfLongFun);
InterlockedIncrement(&maxNumOfThread);
}
} else{//若不是时长任务
thread = getIdleThread();
}
if(thread!=NULL){
thread->executeTask(waitTask->task,waitTask->param,waitTask->taskCb);
delete waitTask;
moveThreadToBusyList(thread);
} else{
waitTaskLock.Lock();
waitTaskList.push_front(waitTask);
waitTaskLock.UnLock();
}
}
从代码中可以看出,它主要作用是 从空闲线程队列中,取出一个线程,取空闲线程的动作是线程安全的,如果空闲线程队列为空,且当前线程池中的线程不超过最大线程数的情况下,会新开一个线程。 然后调用该线程的执行任务的方法。 注意这一过程应该是异步的,并不是调度线程去执行,他只是设置好相应的参数,并触发线程。其代码如下:
void ThreadPool::Thread::executeTask(TaskFun task, PVOID param, TaskCallbackFun taskCallback) {
busy = TRUE;
this->task = task;
this->param = param;
this->taskCb = taskCallback;
ResumeThread(thread);
}
嗯,整体上就是这样的一个情况了。 呃,对了,任务没讲呢,很简单,其代码如下:
class Task {
public:
static int task1(PVOID param);
};
class TaskCallBack{
public:
static void taskCallback1(int result);
};
没啥特殊要求,只要满足函数指针的 类型限定即可。