【Linux网络编程】高并发服务器编程 -- 进程池与线程池

【Linux网络编程】高并发服务器编程 -- 进程池与线程池

【1】动态创建进程/线程实现高并发的缺点

  1. 耗费时间,响应慢;
  2. 动态创建的子进程/子线程通常只能为一个客户服务,将导致系统产生大量的细微进程/线程,进程/线程切换将会耗费大量CPU时间;
  3. 当前进程必须合理管理其分配的各项资源,否则有可能导致可用资源衰竭;

【2】进程池与线程池概述

进程池/线程池由系统预先创建一组进程(数目在3-10个)/线程(数目与CPU数量相当),当新的任务到来时,主进程通过某种方式选择池中的某一个进程/线程完成任务;

选择池中进程/线程方式

  1. 随机算法、Round Robin(轮流选取)算法等选择工作进程/线程;
  2. 共享工作队列方式实现同步,子进程/线程睡眠在工作队列上,当新任务到来,主进程将任务放入工作队列中,同时唤醒一个工作进程/线程处理任务(其余工作进程/线程仍处于睡眠状态);

主进程与工作进程/线程传递数据方式

  1. 主进程与子进程通过管道的方式实现进程间通信并共享数据;
  2. 主进程与子线程之间通过将待共享的数据设置为全局变量的方式实现共享;

【Linux网络编程】高并发服务器编程 -- 进程池与线程池_第1张图片

处理多客户注意事项

  1. 客户状态是无状态的,可以使用不同的子进程/子线程为客户的不同请求服务;
  2. 客户任务存在上下文关系,则应该使用同一个子进程/子线程为客户提供服务;

【3】半同步/半异步模式

同步/异步概念

I/O模型中,异步指内核向应用程序通知完成事件,由内核实现读写操作;同步指内核向应用程序通知就绪事件,由应用程序实现读写操作;

并发模式中,异步指程序的执行由系统事件驱动;同步指程序完全按照代码序列的顺序执行;

【Linux网络编程】高并发服务器编程 -- 进程池与线程池_第2张图片

同步/异步线程概念

同步线程,按照同步方式运行的线程;异步线程,按照异步方式运行的线程;

半同步/半异步模式处理流程

半同步/半异步模式处理流程如下,其中同步线程用于处理客户逻辑,异步线程用于处理I/O事件;

【Linux网络编程】高并发服务器编程 -- 进程池与线程池_第3张图片

半同步/半反应堆模式

【Linux网络编程】高并发服务器编程 -- 进程池与线程池_第4张图片

该模式中,异步线程为主线程,负责监听所有 socket 上的事件;1. 监听 socket 存在可读事件,主线程接受并产生新的连接 socket 并向 epoll 内核事件表中注册该 连接 socket 的读写事件;2. 连接 socket 存在读写事件,主线程将该连接 socket 插入请求队列中;

工作线程为同步线程,睡眠在请求队列上,当有新任务到来时,工作线程通过竞争获取任务接管权并处理任务;

注意,请求队列中的 socket 都是就绪 socket ,工作线程需完成从 socket 上读取客户请求以及往 socket 上写入服务器应答的操作;

半同步/半反应堆模式缺点

  1. 主线程与工作线程共享请求队列,需要对队列加锁保护,浪费 CPU 时间;
  2. 每个工作线程同一时间只能处理一个客户请求,当客户量较多而工作线程较少时,请求队列中将堆积很多任务对象从而导致客户端响应变慢,若增加工作线程则工作线程之间的切换将耗费大量 CPU 时间;

示例代码

半同步半反应堆线程池的实现

#ifndef THREADPOOL_H
#define THREADPOOL_H

#include 
#include 
#include 
#include 
#include "locker.h"
/**
 * 线程池类
 * 此处定义为模板类是为了代码复用;
 */
template< typename T >
class threadpool
{
public:
    /**
     * 线程池构造函数
     * thread_number    : 线程池中线程的数量;
     * max_requests     : 请求队列中最多允许的、等待处理的请求的数量;
     */
    threadpool( int thread_number = 8, int max_requests = 10000 );
    /**
     * 线程池析构函数;
     */
    ~threadpool();
    /**
     * 该函数用于向队列中添加任务;
     */
    bool append( T* request );

private:
    /**
     * 该函数用于创建工作线程;
     */
    static void* worker( void* arg );
    /**
     * 该函数用于运行线程池中的线程;
     */
    void run();

private:
    int m_thread_number;            // 线程池中的线程数;
    int m_max_requests;             // 请求队列中允许的最大的请求数;
    pthread_t* m_threads;           // 线程池中的数组,用于存放线程池中的线程;
    std::list< T* > m_workqueue;    // 请求队列;
    locker m_queuelocker;           // 保护请求队列的互斥锁;
    sem m_queuestat;                // 用于表示是否存在需要处理的任务;
    bool m_stop;                    // 用于表示是否需要结束进程;
};
/**
 * 线程池构造函数
 */
template< typename T >
threadpool< T >::threadpool( int thread_number, int max_requests ) : 
        m_thread_number( thread_number ), m_max_requests( max_requests ), 
        m_stop( false ), m_threads( NULL )
{
    if( ( thread_number <= 0 ) || ( max_requests <= 0 ) )
    {
        throw std::exception();
    }

    m_threads = new pthread_t[ m_thread_number ];
    if( ! m_threads )
    {
        throw std::exception();
    }

    for ( int i = 0; i < thread_number; ++i )
    {
        printf( "create the %dth thread\n", i );
        /**
         * 原型 : int pthread_create(pthread_t  *restrict tidp, const  pthread_attr_t  *restrict_attr,
         *    void*(*start_rtn)(void*),   void   *restrict   arg);
         * 
         * 第一个参数为指向线程标识符的指针;
         * 第二个参数用来设置线程属性;
         * 第三个参数是线程运行函数的地址;
         * 第四个参数是运行函数的参数;
         *
         * 返回值
         * 若成功则返回0,否则返回出错编号;
         * 
         * 功能 : 创建一个线程;
         */
        if( pthread_create( m_threads + i, NULL, worker, this ) != 0 )
        {
            delete [] m_threads;
            throw std::exception();
        }
        /**
         * 原型 : int pthread_detach(pthread_t tid)
         * 
         * 功能 : 可回收创建时 detachstate 属性设置为 PTHREAD_CREATE_JOINABLE 的线程的存储空间;
         *      该函数不会阻塞父线程; 
         *      主线程与子线程分离,两者相互不干涉,子线程结束同时子线程的资源自动回收;
         * 
         * 参数 : 分离线程的 tid;
         * 
         * 返回值 : 成功返回 0, 失败返回错误号;
         */
        if( pthread_detach( m_threads[i] ) )
        {
            delete [] m_threads;
            throw std::exception();
        }
    }
}
/**
 * 线程池析构函数
 * 删除线程占据的内存空间
 */
template< typename T >
threadpool< T >::~threadpool()
{
    delete [] m_threads;
    m_stop = true;
}
/**
 * 该函数用于向队列中添加任务;
 */
template< typename T >
bool threadpool< T >::append( T* request )
{
    // 队列加锁保护
    m_queuelocker.lock();
    if ( m_workqueue.size() > m_max_requests )
    {
        m_queuelocker.unlock();
        return false;
    }
    // 任务入队
    m_workqueue.push_back( request );
    // 释放锁
    m_queuelocker.unlock();
    // 信号量加一,表明队列中存在待处理的事件
    m_queuestat.post();
    return true;
}
/**
 * 该函数用于创建工作线程
 */
template< typename T >
void* threadpool< T >::worker( void* arg )
{
    threadpool* pool = ( threadpool* )arg;
    pool->run();
    return pool;
}
/**
 * 该函数用于运行线程池中的线程
 */
template< typename T >
void threadpool< T >::run()
{
    while ( ! m_stop )
    {
        // 信号量减一
        m_queuestat.wait();
        // 队列加锁保护
        m_queuelocker.lock();
        // 判断任务队列是否为空
        if ( m_workqueue.empty() )
        {
            m_queuelocker.unlock();
            continue;
        }
        // 获取队列首个任务元素
        T* request = m_workqueue.front();
        // 删除队列中的第一个元素
        m_workqueue.pop_front();
        // 队列解除锁保护
        m_queuelocker.unlock();
        if ( ! request )
        {
            continue;
        }
        /**
         * 调用任务结构体中的处理函数处理任务逻辑
         */
        request->process();
    }
}

#endif

高效的半同步/半异步模式

【Linux网络编程】高并发服务器编程 -- 进程池与线程池_第5张图片

该模式中,主线程为异步线程,只负责监听 socket,连接 socket 由工作线程管理,当有新的连接到来时,主线程接受该连接并将产生的新的连接 socket 分发给工作线程,此后该连接 socket 上的任何 I/O 操作都由被选中的工作线程处理,直到客户关闭连接;

主线程通过与工作线程之间的管道传递数据,当工作线程检测到管道有数据可读时,便分析该数据是否是新的客户连接请求,若是则将该新的 socket 上的读写事件注册到自己的 epoll 内核事件表中;

该高效半同步/半异步模式特点,每个线程(主线程和工作线程)都维持自己的事件循环,他们各自独立地监听不同的事件;

示例代码

半同步/半异步进程池实现

#ifndef PROCESSPOOL_H
#define PROCESSPOOL_H

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 

/**
 * 该类用于描述子进程
 */ 
class process
{
public:
    process() : m_pid( -1 ){}

public:
    /**
     * m_pid    : 目标子进程的 PID
     * m_pipefd : 父进程与子进程之间通信的通道
     */ 
    pid_t m_pid;
    int m_pipefd[2];
};

/**
 * 进程池类,将它定义成模板类目的是方便代码复用;
 * 其模板参数是处理逻辑任务的类;
 */ 
template< typename T >
class processpool
{
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 内核事件表的标识
    int m_epollfd;
    // 监听 socket 描述符
    int m_listenfd;
    // 子进程通过 m_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];

/**
 * 该函数用于设置文件描述符为非阻塞;
 * 
 * F_GETFL : 获取 fd 的状态标志
 * F_SETFL : 设置 fd 的状态标志
 */ 
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;
}
/**
 * 该函数用于向 epoll 文件描述符添加文件描述符 fd;
 * EPOLLIN : 对应的文件描述符可以读;
 * EPOLLET : 对应的文件描述符设定为 edge 模式
 */ 
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 );
}
/**
 * 功能 : 从 epollfd 上删除 fd 对应的事件,并关闭文件描述符;
 * EPOLL_CTL_DEL : 删除 epollfd 上的事件;
 */
static void removefd( int epollfd, int fd )
{
    epoll_ctl( epollfd, EPOLL_CTL_DEL, fd, 0 );
    close( fd );
}
/**
 * 信号处理函数
 * 
 * 信号处理函数中将信号代码发送到 sig_pipefd[1] 中;
 */
static void sig_handler( int sig )
{
    int save_errno = errno;
    int msg = sig;
    send( sig_pipefd[1], ( char* )&msg, 1, 0 );
    errno = save_errno;
}
/**
 * 该函数用于设置信号处理函数
 * 
 * 原型 : sigaction(int sig, const struct sigaction* act, struct sigaction* oact)
 * sig : 待捕获的信号类型;
 * act : 新的信号处理方式;
 * oact : 信号先前的处理方式
 */
static void addsig( int sig, void( handler )(int), bool restart = true )
{
    struct sigaction sa;
    memset( &sa, '\0', sizeof( sa ) );
    sa.sa_handler = handler;
    if( restart )
    {
        sa.sa_flags |= SA_RESTART;
    }
    sigfillset( &sa.sa_mask );
    assert( sigaction( sig, &sa, NULL ) != -1 );
}
/**
 * 进程池的构造函数
 * 
 * m_idx = -1,表示当前进程为父进程;
 */ 
template< typename T >
processpool< T >::processpool( int listenfd, int process_number ) 
    : m_listenfd( listenfd ), m_process_number( process_number ), m_idx( -1 ), 
    m_stop( false )
{
    assert( ( process_number > 0 ) && ( process_number <= MAX_PROCESS_NUMBER ) );
    // process 类类型数组,用于保存所有子进程的描述信息
    m_sub_process = new process[ process_number ];
    assert( m_sub_process );

    for( int i = 0; i < process_number; ++i )
    {
        /**
         * socketpair : 用于创建一对无名的、相互连接的套接字
         * 如果函数成功,则返回0,创建好的套接字分别是sv[0]和sv[1];
         * 否则返回-1,错误码保存于errno中;
         * 
         * 基本用法: 
         * 1. 这对套接字可以用于全双工通信,每一个套接字既可以读也可以写;
         * 例如,可以往sv[0]中写,从sv[1]中读;或者从sv[1]中写,从sv[0]中读; 
         * 2. 如果往一个套接字(如sv[0])中写入后,再从该套接字读时会阻塞,
         * 只能在另一个套接字中(sv[1])上读成功; 
         * 3. 读、写操作可以位于同一个进程,也可以分别位于不同的进程,如父子进程;
         * 如果是父子进程时,一般会功能分离,一个进程用来读,一个用来写。
         * 因为文件描述符sv[0]和sv[1]是进程共享的,所以读的进程要关闭写描述符, 
         * 反之,写的进程关闭读描述符。 
         * 
         * AF_UNIX用于同一台机器上的进程间通信;
         * SOCK_STREAM提供的稳定数据传输,即TCP协议;
         * 
         */ 
        int ret = socketpair( PF_UNIX, SOCK_STREAM, 0, m_sub_process[i].m_pipefd );
        assert( ret == 0 );
        /**
         * 创建子进程
         * 
         * fork 仅仅被调用一次,却能够返回两次;
         * 返回值意义 : 
         * 1)在父进程中,fork 返回新创建子进程的进程ID;
         * 2)在子进程中,fork 返回 0;
         * 3)如果出现错误,fork 返回一个负值;
         * 
         * 注意,说明 : 
         * 不管是局部的还是全局的变量或者是锁,fork() 之后,子进程都拥有了一份父进程的拷贝
         */ 
        m_sub_process[i].m_pid = fork();
        assert( m_sub_process[i].m_pid >= 0 );
        if( m_sub_process[i].m_pid > 0 )
        {
            /**
             * 父进程中相关变量的处理
             * 父进程中关闭写通道
             */
            close( m_sub_process[i].m_pipefd[1] );
            continue;
        }
        else
        {
            /**
             * 子进程中相关变量的处理
             * 子进程中关闭读通道
             * m_idx 存放在子进程中,父进程中的 m_idx 变量不受到影响
             */
            close( m_sub_process[i].m_pipefd[0] );
            m_idx = i;
            break;
        }
    }
}
/**
 * 该函数用于创建信号通信的管道
 */ 
template< typename T >
void processpool< T >::setup_sig_pipe()
{
    // 创建 epollfd 描述符
    m_epollfd = epoll_create( 5 );
    assert( m_epollfd != -1 );
    // 创建一对无名的、相互连接的套接字
    int ret = socketpair( PF_UNIX, SOCK_STREAM, 0, sig_pipefd );
    assert( ret != -1 );
    // 将管道 1 设置为非阻塞
    // 将管道 0 加入 epoll 事件中
    setnonblocking( sig_pipefd[1] );
    addfd( m_epollfd, sig_pipefd[0] );
    /**
     * 添加信号处理函数
     * 
     * SIGCHLD  : 在一个进程终止或者停止时,将 SIGCHLD 信号发送给其父进程;
     * 按系统默认将忽略此信号,如果父进程希望被告知其子系统的这种状态,则应捕捉此信号;
     * SIGTERM  : 是不带参数时kill发送的信号,意思是要进程终止运行,
     * 但执行与否还得看进程是否支持;
     * SIGINT   : 中断信号,终端在用户按下 CTRL + C 发送到前台进程;
     * 默认行为是终止进程,但它可以捕获或忽略;
     */ 
    addsig( SIGCHLD, sig_handler );
    addsig( SIGTERM, sig_handler );
    addsig( SIGINT, sig_handler );
    // 忽视 SIGPIPE 信号
    addsig( SIGPIPE, SIG_IGN );
}
/**
 * 该函数用于运行进程池中的进程
 */ 
template< typename T >
void processpool< T >::run()
{
    /**
     * m_id 用于区分父进程与子进程
     */
    if( m_idx != -1 )
    {
        run_child();
        return;
    }
    run_parent();
}
/**
 * 子进程运行函数
 */ 
template< typename T >
void processpool< T >::run_child()
{
    // 建立信号通信管道
    setup_sig_pipe();
    /**
     * m_sub_process[m_idx].m_pipefd[ 1 ] : 在进程池创建构建函数中初始化;
     * 该变量代表子进程的读通道,并加入到 epoll 的事件监听中;
     */ 
    int pipefd = m_sub_process[m_idx].m_pipefd[ 1 ];
    addfd( m_epollfd, pipefd );

    epoll_event events[ MAX_EVENT_NUMBER ];
    T* users = new T [ USER_PER_PROCESS ];
    assert( users );
    int number = 0;
    int ret = -1;

    while( ! m_stop )
    {
        // 等待事件发生,number 表示事件发生的数目;
        number = epoll_wait( m_epollfd, events, MAX_EVENT_NUMBER, -1 );
        // 事件选择发生错误;
        if ( ( number < 0 ) && ( errno != EINTR ) )
        {
            printf( "epoll failure\n" );
            break;
        }
        // 遍历处理每一个事件
        for ( int i = 0; i < number; i++ )
        {
            // 获取 epoll 中的文件描述符;
            int sockfd = events[i].data.fd;
            /**
             * 父进程给子进程发送通知的情况; 
             * EPOLLIN : 对应的文件描述符可以读;
             */
            if( ( sockfd == pipefd ) && ( events[i].events & EPOLLIN ) )
            {
                /**
                 * 接收客户端发送的数据;
                 * 
                 * EAGAIN : 非阻塞的系统调用,由于资源限制/不满足条件, 返回的错误号;
                 */ 
                int client = 0;
                ret = recv( sockfd, ( char* )&client, sizeof( client ), 0 );
                /**
                 * 
                 */ 
                if( ( ( ret < 0 ) && ( errno != EAGAIN ) ) || ret == 0 ) 
                {
                    continue;
                }
                else
                {
                    /**
                     * 接收客户端连接,将连接描述符加入 epoll 事件监听
                     */ 
                    struct sockaddr_in client_address;
                    socklen_t client_addrlength = sizeof( client_address );
                    int connfd = accept( m_listenfd, ( struct sockaddr* )
                    &client_address, &client_addrlength );
                    if ( connfd < 0 )
                    {
                        printf( "errno is: %d\n", errno );
                        continue;
                    }
                    addfd( m_epollfd, connfd );
                    /**
                     * 模板类 T 必须实现 init 方法,以初始化一个客户连接;
                     * 此处直接使用 connfd 来索引逻辑处理对象,从而提高程序效率;
                     */ 
                    users[connfd].init( m_epollfd, connfd, client_address );
                }
            }
            /**
             * 处理子进程接收到的信号的情况;
             * EPOLLIN : 对应的文件描述符可以读;
             */ 
            else if( ( sockfd == sig_pipefd[0] ) && ( events[i].events & EPOLLIN ) )
            {
                int sig;
                char signals[1024];
                // 接收相应信号处理函数中发送到管道的信号信息,ret 读入的字节数;
                ret = recv( sig_pipefd[0], signals, sizeof( signals ), 0 );
                if( ret <= 0 )
                {
                    continue;
                }
                else
                {
                    for( int i = 0; i < ret; ++i )
                    {
                        // 遍历接收到的字节
                        switch( signals[i] )
                        {
                            // 子进程终止信号
                            case SIGCHLD:
                            {
                                pid_t pid;
                                int stat;
                                /**
                                 * 避免仍然存在僵死进程;
                                 * waitpid : 会暂时停止目前进程的执行,
                                 * 直到有信号来到或子进程结束;
                                 * WNOHANG : 若 pid 指定的子进程没有结束,
                                 * 则 waitpid() 函数返回 0,不予以等待;
                                 *           若结束,则返回该子进程的ID;
                                 */ 
                                while ( ( pid = waitpid( -1, &stat, WNOHANG ) ) > 0 )
                                {
                                    continue;
                                }
                                break;
                            }
                            /**
                             * 处理进程终止信号
                             */ 
                            case SIGTERM:
                            case SIGINT:
                            {
                                m_stop = true;
                                break;
                            }
                            default:
                            {
                                break;
                            }
                        }
                    }
                }
            }
            /**
             * 对于其他可读信号的情况,调用逻辑处理对象的 process 方法处理逻辑业务;
             */ 
            else if( events[i].events & EPOLLIN )
            {
                 users[sockfd].process();
            }
            else
            {
                continue;
            }
        }
    }
    // 循环终止,清除所有的 users 关闭文件描述符;
    delete [] users;
    users = NULL;
    close( pipefd );
    /**
     * 由创建者负责关闭该文件描述符;
     * 即所谓的对象(比如一个文件描述符,又或者一段堆内存)
     * 由哪个函数创建,就应该由哪个函数销毁;
     */ 
    //close( m_listenfd );
    close( m_epollfd );
}

template< typename T >
void processpool< T >::run_parent()
{
    // 建立信号处理相关的管道;
    setup_sig_pipe();
    // 将监听事件添加到 epoll 事件表中,监听客户端连接请求的事件;
    addfd( m_epollfd, m_listenfd );

    epoll_event events[ MAX_EVENT_NUMBER ];
    int sub_process_counter = 0;
    int new_conn = 1;
    int number = 0;
    int ret = -1;

    while( ! m_stop )
    {
        // epoll 等待事件发生
        number = epoll_wait( m_epollfd, events, MAX_EVENT_NUMBER, -1 );
        if ( ( number < 0 ) && ( errno != EINTR ) )
        {
            printf( "epoll failure\n" );
            break;
        }
        // 遍历发生的事件
        for ( int i = 0; i < number; i++ )
        {
            int sockfd = events[i].data.fd;
            /**
             * 处理新的连接请求;
             * 采用轮流选取的方式选区子进程;
             */ 
            if( sockfd == m_listenfd )
            {
                int i =  sub_process_counter;
                do
                {
                    // m_sub_process[i].m_pid != -1 : 进程池中的该子进程未退出;
                    if( m_sub_process[i].m_pid != -1 )
                    {
                        break;
                    }
                    i = (i+1)%m_process_number;
                }
                while( i != sub_process_counter );
                /**
                 * 对于所有子进程全都遍历结束的情况
                 * 此时表明当前进程池不存在仍然存活的子进程
                 * 应该退出
                 */ 
                if( m_sub_process[i].m_pid == -1 )
                {
                    m_stop = true;
                    break;
                }
                sub_process_counter = (i+1)%m_process_number;
                // 向子进程发送请求,表示由新的连接请求到来;
                send( m_sub_process[i].m_pipefd[0], ( char* )&new_conn, 
                sizeof( new_conn ), 0 );
                printf( "send request to child %d\n", i );
            }
            // 处理父进程接收到的信号
            else if( ( sockfd == sig_pipefd[0] ) && ( events[i].events & EPOLLIN ) )
            {
                int sig;
                char signals[1024];
                ret = recv( sig_pipefd[0], signals, sizeof( signals ), 0 );
                if( ret <= 0 )
                {
                    continue;
                }
                else
                {
                    for( int i = 0; i < ret; ++i )
                    {
                        switch( signals[i] )
                        {
                            // 处理僵死进程
                            case SIGCHLD:
                            {
                                pid_t pid;
                                int stat;
                                /**
                                 * 避免仍然存在僵死进程;
                                 * waitpid : 会暂时停止目前进程的执行,
                                 * 直到有信号来到或子进程结束;
                                 * WNOHANG : 若 pid 指定的子进程没有结束,
                                 * 则 waitpid() 函数返回 0,不予以等待;
                                 *           若结束,则返回该子进程的ID;
                                 */
                                while ( ( pid = waitpid( -1, &stat, WNOHANG ) ) > 0 )
                                {
                                    for( int i = 0; i < m_process_number; ++i )
                                    {
                                        /**
                                         * 若进程池中第 i 个子进程退出,
                                         * 则主进程关闭相应的通信管道,
                                         * 并设置相应的 m_pid 为 -1,以标识该子进程已经退出;
                                         */ 
                                        if( m_sub_process[i].m_pid == pid )
                                        {
                                            printf( "child %d join\n", i );
                                            close( m_sub_process[i].m_pipefd[0] );
                                            m_sub_process[i].m_pid = -1;
                                        }
                                    }
                                }
                                /**
                                 * 判断当前进程池中是否仍然存在尚未退出的子进程;
                                 * 若当前进程池中所有的子进程都退出则父进程也应该退出;
                                 */ 
                                m_stop = true;
                                for( int i = 0; i < m_process_number; ++i )
                                {
                                    if( m_sub_process[i].m_pid != -1 )
                                    {
                                        m_stop = false;
                                    }
                                }
                                break;
                            }
                            /**
                             * 进程终止信号的处理;
                             * 需要终止所有的子进程,并等待它们全部结束;
                             */ 
                            case SIGTERM:
                            case SIGINT:
                            {
                                printf( "kill all the clild now\n" );
                                for( int i = 0; i < m_process_number; ++i )
                                {
                                    int pid = m_sub_process[i].m_pid;
                                    if( pid != -1 )
                                    {
                                        kill( pid, SIGTERM );
                                    }
                                }
                                break;
                            }
                            default:
                            {
                                break;
                            }
                        }
                    }
                }
            }
            else
            {
                continue;
            }
        }
    }
    /**
     * 由创建者负责关闭该文件描述符;
     * 即所谓的对象(比如一个文件描述符,又或者一段堆内存)
     * 由哪个函数创建,就应该由哪个函数销毁;
     */ 
    //close( m_listenfd );
    close( m_epollfd );
}

#endif

【4】领导者/追随者模式

基本概念

领导者/追随者模式是多个工作线程轮流获得事件源集合,轮流监听、分发并处理事件的一种模式。
在任意时间点,程序都仅有一个领导者线程,它负责监听IO事件。而其他线程都是追随者,它们休眠在线程池中等待成为新的领导者。当前的领导者如果检测到IO事件,首先要从线程池中推选出新的领导者线程,然后处理IO事件。此时,新的领导者等待新的IO事件,而原来的领导者则处理IO事件,从而实现了并发。

领导者/追随者模式构成示意图

【Linux网络编程】高并发服务器编程 -- 进程池与线程池_第6张图片

  • 句柄集,监听句柄上的 IO 事件,并将就绪事件通知给领导者线程;
    • 句柄表示IO资源,linux下通常是文件描述符。
    • wait_for_event 方法:监听这些句柄上的IO事件,并将其中的就绪事件通知给领导者线程;
    • register_handle 方法:由领导者调用将事件绑定到 Handle 上的事件处理器来处理事件;
  • 线程集,所有工作线程的管理者,负责线程同步、推选新领导;
    • 线程集中线程在任一时刻的状态 :
      • Leader:领导者线程,负责等待句柄集上的IO事件;
      • Processing:线程正在处理事件,领导者检测到 IO 事件后可以转移至 Processing 状态处理该事件,并调用 promote_new_leader 方法推选新领导者;也可以指定其他追随者来处理事件,此时领导者地位不变;当处于 Processing 状态的线程处理完事件后,如果当前线程集中没有领导者,则它将成为新领导者,否则它直接转为追随者;
      • Follower:线程处于追随者身份,通过调用线程集的 join 方法等待成为新领导者,也可能被领导者指定来处理新的事件;

【Linux网络编程】高并发服务器编程 -- 进程池与线程池_第7张图片

  • 事件处理器和具体的事件处理器
    • 事件处理器通常包含一个或多个回调函数handle_event;这些回调函数用于处理事件对应的业务逻辑;事件处理器在使用前需要被绑定到某个句柄上,当该句柄有事件发生时,领导者就执行绑定的事件处理器的回调函数;具体的事件处理器是事件处理器的派生类。它们重新实现基类的handle_event方法,以处理特定的任务;

领导者/追随者模式工作流程图

【Linux网络编程】高并发服务器编程 -- 进程池与线程池_第8张图片

注,领导者/追随者模式存在一个明显的缺点即该模式仅支持一个事件源集合,这样工作线程将无法独立地管理多个客户连接。

参考
本博客为博主的学习实践总结,并参考了众多博主的博文,在此表示感谢,博主若有不足之处,请批评指正。

【1】UNIX网络编程

【2】Linux 高性能服务器编程

 

你可能感兴趣的:(网络编程)