在一般的设计中,当需要一个线程时,就创建一个,但是当线程过多时可能会影响系统的整体效率,这个性能的下降主要体现在:当线程过多时在线程间来回切换需要花费时间,而频繁的创建和销毁线程也需要花费额外的机器指令,同时在某些时候极少数线程可能就可以处理大量,比如http服务器可能只需要几个线程就可以处理用户发出的http请求,毕竟相对于用户需要长时间来阅读网页来说,CPU只是找到对应位置的页面返回即可。在这种情况下为每个用户连接创建一个线程长时间等待再次处理用户请求肯定是不划算的。为了解决这种问题,提出了线程池的概念,线程池中保存一定数量的 线程,当需要时,由线程池中的某一个线程来调用对应的处理函数。通过控制线程数量从而减少了CPU的线程切换,而且用完的线程还到线程池而不是销毁,下一次再用时直接从池中取,在某种程度上减少了线程创建与销毁的消耗,从而提高效率
在Windows上,使用线程池十分简单,它将线程池做为一个整体,当需要使用池中的线程时,只需要定义对应的回调函数,然后调用API将回调函数进行提交,系统自带的线程池就会自动执行对应的回调函数。从而实现任务的执行,这种方式相对于传统的VC线程来说,程序员不再需要关注线程的创建与销毁,以及线程的调度问题,这些统一由系统完成,只需要将精力集中到逻辑处理的回调函数中来,这样将程序员从繁杂的线程控制中解放出来。同时Windows中线程池一般具有动态调整线程数量的自主行为,它会根据线程中执行任务的工作量来自动调整线程数,即不让大量线程处于闲置状态,也不会因为线程过少而有大量任务处于等待状态。
在windows上主要有四种线程池
1. 普通线程池
2. 同步对象等待线程池
3. 定时器回调线程池
4. 完成端口回调线程池
这些线程池最大的特点是需要提供一个由线程池中线程调用的回调函数,当条件满足时回调函数就会被线程池中的对应线程进行调用。从设计的角度来说,这样的设计大大简化了应用程序考虑多线程设计时的难度,此时只需要考虑回调函数中的处理逻辑和被调用的条件即可,而不必考虑线程的创建销毁等等问题(一些设计还可以绕开繁琐的同步处理)。需要注意的就是一般不要在这些回调函数中设计处理类似UI消息循环那样的循环,即不要长久占用线程池中的线程。
下面来依次说明各种线程池的使用:
普通线程池
普通线程池在使用时主要是调用QueueUserWorkItem函数将回调函数加入线程池队列,线程池中一旦有空闲的线程就会调用这个回调,函数原型如下:
第一个参数是一个回调函数地址,函数原型与线程函数原型相同,所以在设计时可以考虑使用宏开关来指定这个回调函数作为线程函数还是作为线程池的回调函数
第二个参数是传给回调函数的参数指针
第三个参数是一个标志值,它的主要值及其含义如下:
标志 | 含义 |
WT_EXECUTEDEFAULT | 线程池的默认标志 |
WT_EXECUTEINIOTHREAD | 以IO可警告状态运行线程回调函数 |
WT_EXECUTEINPERSISTENTTHREAD | 该线程将一直运行而不会终止 |
WT_EXECUTELONGFUNCTION | 执行一个运行时间较长的任务(这会使系统考虑是否在线程池中创建新的线程) |
WT_TRANSFER_IMPERSONATION | 以当前的访问字串运行线程并调用回调函数 |
下面是一个具体的例子:
这段代码上我们加入了WT_EXECUTELONGFUNCTION标识,其实在计算机中,只要达到毫秒级的,这个时候已经达到了系统进行线程切换的时间粒度,这个时候它就是一个需要长时间执行的任务
定时器回调线程池
定时器回调主要经过下面几步:
1. 调用CreateTimerQueue:创建定时器回调的队列
2. 调用CreateTimerQueueTimer创建一个指定时间周期的计时器对象,并指定对应的回调函数及参数
之后当指定的时间片到达,就会将对应的回调历程放入到队列中,一旦线程池中有空闲的线程就执行它
另外可以调用对应的函数对其进行相关的操作:
1. 可以调用ChangeTimerQueueTimer修改一个已有的计时器对象的计时周期
2. 调用DeleteTimerQueueTimer删除一个计时器对象
3. 调用DeleteTimerQueue删除这样一个线程池对象,在删除这个线程池的时候它上面绑定的回调也会被删除,所以在编码时可以直接删除线程池对象而不用调用DeleteTimerQueueTimer删除每一个绑定的计时器对象。但是为了编码的完整性,最好加上删除计时器对象的操作
下面是一个使用的具体例子
上述的代码中我们定义了一个同步事件对象,这个事件对象将在定时器历程中设置为有信号,这样方便我们在主线程中等待计时器历程执行完成
同步对象等待线程池
使用同步对象等待线程池只需要调用函数RegisterWaitForSingalObject,将一个同步对象绑定,当这个同步对象变为有信号或者等待的时间到达时,会调用对应的回调历程。该函数原型如下:
第一个参数是一个输出参数,返回一个等待对象的句柄,我们可以将其看做这个线程池的句柄
第二个参数是一个同步对象
第三个参数是对应的回调函数
第四个参数是传入到回调函数中的参数指针
第五个参数是等待的时间
第六个参数是一个标志与函数QueueUserWorkItem中的标识含义相同
对应回调函数的原型如下:
当同步对象变为有信号或者等待的时间到达时都会调用这个回调,它的第二个参数就表示它所等待的对象是否为有信号。
下面是一个使用的例子
完成端口线程池
在前面讲述文件操作的博文中,讲解了在文件中完成端口的使用,其实完成端口本质上就是一个线程池,或者说,windows上自带的线程池是使用完成端口的基础之上编写的。所以在这,完成端口线程池的使用将比IO完成端口来的简单
通过调用BindIoCompletionCallback函数来将一个IO对象句柄与对应的完成历程绑定,这样在对应的IO操作完成后,对应的历程将会被丢到线程池中准备执行
相比于前面的文件中的完成端口,这个完成端口线程池要简单许多,文件的完成端口需要自己创建完成多个线程,创建完成端口,并且将线程与完成端口绑定。另外还需要在线程中调用相应的等待函数等待IO操作完成,而线程池则不需要这些操作,我只需要准备一个完成历程,然后调用BindIoCompletionCallback,这样一旦历程被调用,就可以肯定IO操作一定完成了。这样我们只需要将主要精力集中在完成历程的编写中
函数BindIoCompletionCallback的原型如下:
第一个参数是一个对应IO操作的句柄
第二个参数是对应的完成历程函数指针
第三个参数是一个标志,与之前的标识相同
完成历程的函数原型如下:
第一个参数是一个错误码,当IO操作发生错误时可以通过这个参数获取当前错误原因
第二个参数是当前IO操作操作的字节数
第三个参数是一个OVERLAPPED结构
这函数的使用与之前文件完成端口中完成历程一样
下面我们将之前文件完成端口的例子进行改写,如下: