在实现并发服务器时,动态创建子进程(子线程)显然有很多缺点,在上篇文章实现TCP多进程(多线程)版本时,就遇到了这个问题,回顾一下之前讲过的缺点:
1、 动态创建进程(或线程)是比较耗费时间的,这将导致较慢的客户响应。
2、进程(或者线程)间的切换消费大量CPU时间。
3、由于系统的资源有限,能够创建的子进程(或线程)的数量有限。
4、动态创建的子进程是当前进程的完整映像。当前进程必须递慎地处理其分配的文件描述符和堆内存等系统资源,否则子进程可能复制这些资源,从而使系统的可用资源急剧下降,进而影响服务器的性能
所以,我们想到是否可以提前创建好一些进程(线程),以提高性能呢?
如果
服务器的硬件资源“充裕”,那么提高服务器性能的一个很直接的方法就是以空间换时间,即“浪费”服务器的硬件资源,以换取其运行效率。这就需要了解池的概念。
什么是池?
池是一组资源的集合,这组资源在服务器启动之初就完全被创建并初始化,这称为静态资源分配。当服务器进入正式运行阶段,即开始处理客户请求的时候,如果它需要相关的资源,就可以直接从池中获取,无需动态分配。
很显然,直接从池中取得所需资源比动态分配资源的速度要快得多,因为分配系统资源的系统调用都是很耗时的。当服务器处理完一个客户连接后,可以把相关的资源放回池中,无需执行系统调用来释放资源。从最终效果来看,池相当于服务器管理系统资源的应用设施,它避免了服务器对内核的频繁访问。
池可以分为多种,常见的有内存池、进程池、线程池和连接池。
注:由于进程池与线程池相似,因此下面我们介绍有关进程池的概念同样适用于线程池。
一、内存池
通常我们习惯直接使用new、malloc等API申请分配内存,我们知道,这样做的话,由于所申请内存块的大小不定,当频繁使用时会造成大量的内存碎片,进而降低性能。
1、概念
内存池(Memory Pool)是一种内存分配方式。内存池是在真正使用内存之前,先申请分配一定数量的、大小相等(一般情况下)的内存块留作备用。当有新的内存需求时,就从内存池中分出一部分内存块,若内存块不够再继续申请新的内存。
也就是说,当程序中需要申请内存的时候,不是直接向操作系统申请,而是直接从内存池中获取;同理,当程序释放内存的时候,并不真正将内存返回给操作系统,而是返回内存池。当程序退出(或者特定时间)时,内存池才将之前申请的真正内存释放。
这样做的一个显著优点是尽量避免了内存碎片问题,还使得内存分配效率得到提升。
2、分类
应用程序自定义的内存池根据不同的适用场景又有不同的类型。
从线程安全的角度来分,内存池可以分为单线程内存池和多线程内存池。
单线程内存池整个生命周期只被一个线程使用,因而不需要考虑互斥访问的问题;
多线程内存池有可能被多个线程共享,因此则需要在每次分配和释放内存时加锁。
相对而言,单线程内存池性能更高,而多线程内存池适用范围更广。
二、进程池(线程池)
1、概念
(1)、进程池是由服务器预先创建的一组子进程,这些子进程的数目在3-10个之间(一般情况);线程池中的线程数量一般和CPU数量差不多。
(2)、进程池中的所有子进程都运行着相同的代码,并具有相同的属性,比如优先级,PGID等。
(3)、相对于动态创建子进程,现在只需选择一个已经存在的子进程,代价要小的多。
那么又有了新的问题,当有新的任务到来时,主进程怎么选择池中的某个子进程为之服务呢?
2、子进程选择算法
(1)、主进程使用某种算法主动选择子进程。最简单常用的算法是随机算法和轮流选取(Round-Robin)算法。
(2)、主进程和所有子进程通过一个共享的工作队列来同步,子进程在该队列上都是睡眠态。当有新的任务到来时,主进程将任务添加到工作队列中。这将唤醒正在等待任务的子进程。
不过只有一个子进程获得新任务的“接管权”,它可以从工作队列中取出并执行它,而其他子进程将继续睡眠在工作队列上。
当选择好子进程后,主进程还需要使用某种通知机制来告诉目标子进程有新进程需要处理,并传递必要的数据。
最简单的方法是,在父进程和子进程之间预先建立好一条管道,然后通过该管道来实现所有进程间通信(当然,要预先定义好一套协议来规范管道的使用)。在父线程和子线程之间传递数据就要简单的多,因为我们可以把这些数据定义全局的,那么他们本身就是被所有线程共享的。