【1】动态创建进程/线程实现高并发的缺点
【2】进程池与线程池概述
进程池/线程池由系统预先创建一组进程(数目在3-10个)/线程(数目与CPU数量相当),当新的任务到来时,主进程通过某种方式选择池中的某一个进程/线程完成任务;
选择池中进程/线程方式
主进程与工作进程/线程传递数据方式
处理多客户注意事项
【3】半同步/半异步模式
同步/异步概念
I/O模型中,异步指内核向应用程序通知完成事件,由内核实现读写操作;同步指内核向应用程序通知就绪事件,由应用程序实现读写操作;
并发模式中,异步指程序的执行由系统事件驱动;同步指程序完全按照代码序列的顺序执行;
同步/异步线程概念
同步线程,按照同步方式运行的线程;异步线程,按照异步方式运行的线程;
半同步/半异步模式处理流程
半同步/半异步模式处理流程如下,其中同步线程用于处理客户逻辑,异步线程用于处理I/O事件;
半同步/半反应堆模式
该模式中,异步线程为主线程,负责监听所有 socket 上的事件;1. 监听 socket 存在可读事件,主线程接受并产生新的连接 socket 并向 epoll 内核事件表中注册该 连接 socket 的读写事件;2. 连接 socket 存在读写事件,主线程将该连接 socket 插入请求队列中;
工作线程为同步线程,睡眠在请求队列上,当有新任务到来时,工作线程通过竞争获取任务接管权并处理任务;
注意,请求队列中的 socket 都是就绪 socket ,工作线程需完成从 socket 上读取客户请求以及往 socket 上写入服务器应答的操作;
半同步/半反应堆模式缺点
示例代码
半同步半反应堆线程池的实现
#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
高效的半同步/半异步模式
该模式中,主线程为异步线程,只负责监听 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事件,从而实现了并发。
领导者/追随者模式构成示意图
领导者/追随者模式工作流程图
注,领导者/追随者模式存在一个明显的缺点即该模式仅支持一个事件源集合,这样工作线程将无法独立地管理多个客户连接。
参考
本博客为博主的学习实践总结,并参考了众多博主的博文,在此表示感谢,博主若有不足之处,请批评指正。
【1】UNIX网络编程
【2】Linux 高性能服务器编程