两种高效的事件处理方式
Reactor模式
Reactor模式要求主线程只负责监听文件描述符的是否有事件发生,有的话就通知工作线程去处理,主线程本身不做任何实质性工作,接受新连接(accept),处理客户请求均在工作线程中完成
使用同步I/O模型的话流程如下:
- 主线程往epoll内核事件表中注册socket上的读就绪事件(这步先不用注册写)
- 调用epoll_wait等待socket上有数据可读.
- 当socket上有数据可读时,epoll_wait通知主线程,主线程则将socket可读事件放入请求队列
- 线程池中某个睡眠状态的线程被唤醒,从socket读取数据,并处理客户请求,然后往epoll内核事件表中注册socket上的写就绪事件.
- 主线程调用epoll_wait等待socket可写
- 当socket可写时,epoll_wait通知主线程,主线程则将socket可写事件放入请求队列
- 线程池中某个睡眠状态的线程被唤醒,往socket写入服务器处理客户请求的结果
注意一点,读写用两个线程分开执行,但线程不区分只读线程和只写线程.
Proactor模式
Proactor模式相反,要求主线程处理所有的I/O操作,工作线程仅仅负责业务逻辑.
使用异步I/O模型实现的Proactor模式工作流程:
- 主线程调用aio_read函数想内核注册socket上的读完成事件,并告诉内核用户读缓冲区的位置(这是为了能让读到的内容复制到用户缓冲区,如何实现),以及读操作完成时如何通知应用程序(一种方法用signal)
- 主线程继续处理其他逻辑
- 读入用户缓冲区后,内核将向应用程序发送一个信号,以通知应用程序数据可用.
- 收到信号,应用程序预先定义好的信号处理函数选择一个工作线程来处理客户请求,工作线程处理完客户请求后,调用aio_write(异步)函数向内核注册写完成事件,并告诉内核用户写缓冲区的位置,以及写操作完成时如何通知应用程序.(信号)
- 主线程继续处理其他逻辑
- 当用户缓冲区的数据被写入socket之后,内核将向应用程序,发送一个信号,以通知应用程序数据已经发送完毕.
- 应用程序预先定义好的信号处理函数选择一个工作线程来做善后处理,比如决定是否关闭socket.
同步I/O实现Proactor模式
主线程执行数据读写操作,主线程向工作线程通知这一”完成事件”.那么从工作线程的角度来看,他们就直接获得了数据读写的结果,接下来要做的只是对读写的结果进行逻辑处理.
与Reactor唯一的不同就是主线程读完socketfd中的内容,打包成请求对象入请求队列,然后唤醒工作线程来处理,然后向epoll中注册写就绪事件,接着主线程执行写操作.
两种高效的并发模式
并发模式主要用来解决I/O密集型的情况,如果是计算密集型,用多线程反而由于任务切换过频繁导致效率降低.I/O操作的速度没有CPU的计算速度快,所以阻塞在I/O上会浪费CPU计算时间.如果一个线程阻塞在I/O,那么由操作系统调度该线程CPU占用率减小,给其他线程提高CPU占用率.
半同步/半异步模式
在这种模式中,同步线程主要用来处理客户逻辑,相当于逻辑单元,异步线程用来处理IO事件,相当于IO处理单元.异步线程(主线程)监听到请求,封装成请求对象插入请求队列,请求队列将通知同步工作线程来处理,同步工作线程不会被中断.
半同步半反应堆模式
特点:
- 异步线程只有一个,由主线程来充当,负责监听所有socket上的事件。
- 如果有新的连接请求,主线程就接受之,以得到新的连接socket
- 在epoll内核事件表中注册该socket上的读写事件
- 如果连接socket上有读写事件发生,即有新的客户请求到来或有数据要发送到客户端,主线程就将该连接socket插入请求队列。
- 所有工作线程都睡眠在请求队列上,当有任务到来时,它们将通过竞争获得任务的接管权。
缺点:
- 主线程和工作线程共享同一个请求队列,所以主线程在插入请求对象时,会加锁保护队列,使得工作线程无法读取请求对象.浪费CPU时间.
- 工作线程同一时间只能处理一个客户请求.
改进:
主线程监听是否有新的连接,有的话就分配给工作线程,工作线程调用epoll_wait,将得到的新连接的事件添加到自己的epoll内核注册表中.
领导者/追随者模式
领导者线程用来监听IO事件,如果检测到事件,首先推举一个新的追随者线程为领导者线程,然后去处理事件.
领导者追随者模式包含如下几个组件:句柄集,线程集,事件处理器,和具体的事件处理器.
- 句柄集:用来表示IO资源,也就是文件描述符的集合
- 线程集:所有线程的管理者,其中的线程处于三种状态之一,leader,follower,processing.
- 事件处理器:一系列的回调函数,这些回调函数用来处理业务逻辑.用之前需要绑定一个文件描述符,这样有事件发生,就执行该事件处理器的回调函数.
- 具体的事件处理器:事件处理的派生类,重载父类的回调函数,用来处理特定的任务.
接下来说明对于http的报文如何解析.
有限状态机
有限状态机就是每次循环执行和状态相对应的逻辑,然后转移到下一种状态,
比如在http报文分析中,我们将主状态分为读请求行和读头部字段,从状态分为每行的读取状态:读完一行,行不完整,行有错误.然后每次循环读一行,对主状态switch,先解析请求行,然后转移主状态,再次循环执行解析头部字段.
提高性能的其他建议
- 池化思想:不要动态分配线程进程,用事先分配好的线程池或进程池,减小系统调用开销
- 数据复制:避免不必要的数据复制,特别在用户空间和内核空间之间.
- 上下文切换和锁:不要使用过多线程,减小上下文切换的开销.不要频繁使用锁,会降低线程的并发性.