进程池
进程池的使用场景
当我们需要并行的处理大规模任务的时候,需要使用到多进程,多线程技术,比如说服务器处理大量客户端的任务,我在大一的时候写过一个C/S+mysql架构的聊天室,大概是这样处理的,每当有客户端发出连接请求时,服务器accept成功以后就去fork一个进程去处理关于这个客户端的所有请求,经过后来的一系列的学习,这显然是极其不科学的,狂开进程浪费资源且不说,效率也极低。
- 动态创建进程(或线程)是比较耗费时间的,这将导致较慢的客户响应。
- 动态创建的子进程(或子线程)通常只用来为一个客户服务(除非我们做特殊处理),这将导致系统上产生大量的细微进程(或者线程)。进程(或者线程)间的切换消费大量CPU时间。
- 动态创建的子进程是当前进程的完整映像。当前进程必须谨慎地管理其分配的文件描述符和堆内存等系统资源,从而使系统的可用资源急剧下降,进而影响服务器的性能。
池化的思想
为了解决上述问题呀,我们的前辈们相出了用池化的思想来解决这些资源消耗问题。简而言之,就是提前创建好一些进程,一般3-7个(具体看cpu核数以及运行环境),目的是为了充分释放多核芯的性能,实大规模并行。由于服务器的硬件资源“充裕”,那么提高服务器性能的一个很直接的方法就是以空间换时间,即“浪费”服务器的硬件资源,以换取其运行效率。这就是池的概念。池是一组资源的集合,这组资源在服务器启动之初就完全被创建并初始化,这称为静态资源分配。当服务器进入正式运行阶段,即开始处理客户请求的时候,如果它需要相关的资源,就可以直接从池中获取,无需动态分配。很显然,直接从池中取得所需资源比动态分配资源的速度要快得多,因为分配系统资源的系统调用都是很耗时的。当服务器处理完一个客户连接后,可以把相关的资源放回池中,无需执行系统调用来释放资源。从最终效果来看,池相当于服务器管理系统资源的应用设施,它避免了服务器对内核的频繁访问。
进程池概述
进程池中的所有子进程都运行着相同的代码(不调用exev族函数),并具有相同的属性,比如优先级、 PGID 等。
当有新的任务来到时,主进程将通过某种方式选择进程池中的某一个子进程来为之服务。相比于动态创建子进程,选择一个已经存在的子进程的代价显得小得多。至于主进程选择哪个子进程来为新任务服务,则有两种方法:
1)主进程使用某种算法来主动选择子进程。最简单、最常用的算法是随机算法和 Round Robin (轮流算法)。
2)主进程和所有子进程通过一个共享的工作队列来同步,子进程都睡眠在该工作队列上。当有新的任务到来时,主进程将任务添加到工作队列中。这将唤醒正在等待任务的子进程,不过只有一个子进程将获得新任务的“接管权”,它可以从工作队列中取出任务并执行之,而其他子进程将继续睡眠在工作队列上。
当选择好子进程后,主进程还需要使用某种通知机制来告诉目标子进程有新任务需要处理,并传递必要的数据。最简单的方式是,在父进程和子进程之间预先建立好一条管道,然后通过管道来实现所有的进程间通信。在父线程和子线程之间传递数据就要简单的多,因为我们可以把这些数据定义全局的,那么他们本身就是被所有线程共享的。
综合上面的论述,我们将进程池的一般模型描述为下图所示的形式。
处理多客户
在使用进程池处理多客户任务时,首先考虑的一个问题是:监听socket和连接socket是否都由主进程来统一管理。并发模型,其中半同步/半反应堆模式是由主进程统一管理这两种socket的。而高效的半同步/半异步和领导者/追随者模式,则是由主进程管理所有监听socket,而各个子进程分别管理属于自己的连接socket的。对于前一种情况,主进程接受新的连接以得到连接socket,然后它需要将该socket传递给子进程(对于线程池而言,父线程将socket传递给子线程是很简单的。因为他们可以很容易地共享该socket。但对于进程池而言,必须通过管道传输)。后一种情况的灵活性更大一些,因为子进程可以自己调用accept来接受新的连接,这样该父进程就无须向子进程传递socket。而只需要简单地通知一声:“我检测到新的连接,你来接受它。
长连接,即一个客户的多次请求可以复用一个TCP连接。那么,在设计进程池时还需要考虑:一个客户连接上的所有任务是否始终由一个子进程来处理。如果说客户任务是无状态的,那么我们可以考虑使用不同的进程为该客户不同请求服务。
半同步/半异步进程池实现
综合前面的讨论,我们可以实现这个进程池,为了避免在父、子进程之间传递文件描述符,我们将接受新连接的操作放到子进程中,很显然,对于这种模式而言,一个客户连接上的所有任务始终是由一个子进程来处理的。
奉上代码 进程池github地址
进程池模板 方便以后复用
#ifndef PROCESSPOOL_H
#define PROCESSPOOL_H
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <assert.h>
#include <stdio.h>
#include <unistd.h>
#include <errno.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>
#include <sys/epoll.h>
#include <signal.h>
#include <sys/wait.h>
#include <sys/stat.h>
#include <iostream>
/*子进程类*/
class process
{
public:
/*以 -1初始化*/
process() : m_pid( -1 ){
}
public:
/*子进程号*/
pid_t m_pid;
/*父子进程通信管道*/
int m_pipefd[2];
};
/*进程池类
*其模板参数是处理逻辑任务的类
**/
template< typename T >
class processpool
{
/*将构造函数定义为私有的 因此我们只能通过后边的create静态*/
private:
processpool( int listenfd, int process_number = 8 );
public:
/*单例模式 在之后调用到*/
static processpool< T >* create( int listenfd, int process_number = 8 )
{
if( !m_instance )
{
m_instance = new processpool< T >( listenfd, process_number );
}
return m_instance;
}
~processpool()
{
delete [] m_sub_process;
}
/*启动进程池*/
void run();
private:
void setup_sig_pipe();
void run_parent();
void run_child();
private:
/*进程允许的最大子进程数*/
static const int MAX_PROCESS_NUMBER = 16;
/*每个子进程最多能处理的客户数量*/
static const int USER_PER_PROCESS = 65536;
/*epoll 最多能处理的事件数*/
static const int MAX_EVENT_NUMBER = 10000;
/*进程池中的进程数*/
int m_process_number;
/*进程池在池中的序号 从0开始*/
int m_idx;
/*每个进程都有一个epoll内核事件表 用epollfd标识*/
int m_epollfd;
/*监听socket*/
int m_listenfd;
/*子进程通过stop来决定是否停止*/
int m_stop;
/*保存所有的子进程的描述信息*/
process* m_sub_process;
/*进程池静态实例*/
static processpool< T >* m_instance;
};
template< typename T >
processpool< T >* processpool< T >::m_instance = NULL;
/*用于处理信号的管道 实现统一信号源
* 全局 */
static int sig_pipefd[2];
static int setnonblocking( int fd )
{
int old_option = fcntl( fd, F_GETFL );
int new_option = old_option | O_NONBLOCK;
fcntl( fd, F_SETFL, new_option );
return old_option;
}
static void addfd( int epollfd, int fd )
{
epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl( epollfd, EPOLL_CTL_ADD, fd, &event );
setnonblocking( fd );
}
static void removefd( int epollfd, int fd )
{
epoll_ctl( epollfd, EPOLL_CTL_DEL, fd, 0 );
close( fd );
}
static void sig_handler( int sig )
{
int save_errno = errno;
int msg = sig;
/* 这块为啥得转成char* */
send( sig_pipefd[1], ( char* )&msg, 1, 0 );
errno = save_errno;
}
static void addsig( int</