跳转至

异步网络模型

异步网络模型在服务开发中应用非常广泛,相关资料和开源库也非常多。项目中,使用现成的轮子提高了开发效率,除了能使用轮子,还是有必要了解一下轮子的内部构造。

这篇文章从最基础的5种I/O模型切入,到I/O事件处理模型,再到并发模式,最后以Swoole开源库来做具体分析,逐步深入。文中提到的模型都是一些通用的模型,在《linux高性能服务器编程》中也都有涉及。文章不涉及模型的实现细节,最重要的是去理解各个模型的工作模式以及其优缺点。

文中涉及接口调用的部分,都是指Linux系统的接口调用。 共分为5部分:

I/O模型 从基础的系统调用方法出发,给大家从头回顾一下最基本的I/O模型,虽然简单,但是不可或缺的基础; 事件处理模型 这部分在同步I/O、异步I/O的基础上分别介绍Reactor模型以及Proactor模型,着重两种模型的构成以及事件处理流程。Reactor模型是我们常见的;不同平台对异步I/O系统接口的支持力度不同,这部分还介绍了一种使用同步I/O来模拟Proactor模型的方法。并发模式 就是多线程、多进程的编程的模式。介绍了两种较为高效的并发模型,半同步/半异步(包括其演变模式)、Follower/Leader模式。Swoole异步网络模型分析 这部分是结合已介绍的事件处理模型、并发模式对Swoole的异步模型进行分析; 从分析的过程来看,看似复杂的网络模型,可以拆分为简单的模型单元,只不过我们需要权衡利弊,选取合适业务需求的模型单元进行组合。 我们团队基于Swoole 1.8.5版本,做了很多修改,部分模块做了重构,计划在17年6月底将修改后版本开源出去,敬请期待。改善性能的方法 最后一部分是在引入话题,介绍的是几种常用的方法。性能优化是没有终点的,希望大家能贡献一些想法和具体方法。

I/O模型

POSIX 规范中定义了同步I/O 和异步I/O的术语,同步I/O : 需要进程去真正的去操作I/O; 异步I/O :内核在I/O操作完成后再通知应用进程操作结果。

在《UNIX网络编程》中介绍了5中I/O模型:阻塞I/O、非阻塞I/O、I/O复用、SIGIO 、异步I/O;本节对这5种I/O模型进行说明和对比。

I/O阻塞

通常把阻塞的文件描述符(file descriptor,fd)称之为阻塞I/O。默认条件下,创建的socket fd是阻塞的,针对阻塞I/O调用系统接口,可能因为等待的事件没有到达而被系统挂起,直到等待的事件触发调用接口才返回,例如,tcp socket的connect调用会阻塞至第三次握手成功(不考虑socket 出错或系统中断),如图1所示。另外socket 的系统API ,如,accept、send、recv等都可能被阻塞。

img

>                  图1 I/O 阻塞模型示意图
> ```

另外补充一点,网络编程中,通常把可能永远阻塞的系统API调用 称为慢系统调用,典型的如 accept、recv、select等。慢系统调用在阻塞期间可能被信号中断而返回错误,相应的errno 被设置为EINTR,我们需要处理这种错误,解决办法有: **1. 重启系统调用** 直接上示例代码吧,以accept为例,被中断后重启accept 。有个例外,若connect 系统调用在阻塞时被中断,是不能直接重启的(与内核socket 的状态有关),有兴趣的同学可以深入研究一下connect 的内核实现。使用I/O复用等待连接完成,能避免connect不能重启的问题。

c int client_fd = -1;
struct sockaddr_in client_addr;
socklen_t child_addrlen;
while (1) {
call_accept:
client_fd = accept(server_fd,NULL,NULL); if (client_fd < 0) { if (EINTR == errno) { goto call_accept; } else { sw_sysError("accept fail"); break; } } } ``` 2. 信号处理 利用信号处理,可以选择忽略信号,或者在安装信号时设置SA_RESTART属性。设置属性SA_RESTART,信号处理函数返回后,被安装信号中断的系统调用将自动恢复,示例代码如下。需要知道的是,设置SA_RESTART属性方法并不完全适用,对某些系统调用可能无效,这里只是提供一种解决问题的思路,示例代码如下:

int client_fd = -1;  
struct sigaction action,old_action;
action.sa_handler = sig_handler;  
sigemptyset(&action.sa_mask);  
action.sa_flags = 0;  
action.sa_flags |= SA_RESTART;
/// 若信号已经被忽略,则不设置
sigaction(SIGALRM, NULL, &old_action)  
if (old_action.sa_handler != SIG_IGN) {  
    sigaction(SIGALRM, &action, NULL)
}
while (1) {  
    client_fd = accept(server_fd,NULL,NULL)
    if (client_fd < 0) {
        sw_sysError("accept fail");
        break;
    }
}

I/O非阻塞

把非阻塞的文件描述符称为非阻塞I/O。可以通过设置SOCK_NONBLOCK标记创建非阻塞的socket fd,或者使用fcntl将fd设置为非阻塞。

对非阻塞fd调用系统接口时,不需要等待事件发生而立即返回,事件没有发生,接口返回-1,此时需要通过errno的值来区分是否出错,有过网络编程的经验的应该都了解这点。不同的接口,立即返回时的errno值不尽相同,如,recv、send、accept errno通常被设置为EAGIN 或者EWOULDBLOCK,connect 则为EINPRO- GRESS 。

以recv操作非阻塞套接字为例,如图2所示。

img

>                 图2 非阻塞I/O模型示意图
> ```

当我们需要读取,在有数据可读的事件触发时,再调用recv,避免应用层不断去轮询检查是否可读,提高程序的处理效率。通常非阻塞I/O与I/O事件处理机制结合使用。

### I/O复用

最常用的I/O事件通知机制就是I/O复用(I/O multiplexing)。Linux 环境中使用select/poll/epoll 实现I/O复用,I/O复用接口本身是阻塞的,在应用程序中通过I/O复用接口向内核注册fd所关注的事件,当关注事件触发时,通过I/O复用接口的返回值通知到应用程序,如图3所示,以recv为例。I/O复用接口可以同时监听多个I/O事件以提高事件处理效率。

![img](../assets/io---1.png)

> ```
>                图 3 I/O复用模型示意图
> ```

关于select/poll/epoll的对比,可以参考\[\],epoll使用比较多,但是在并发的模式下,需要关注惊群的影响。

### SIGIO

除了I/O复用方式通知I/O事件,还可以通过SIGIO信号来通知I/O事件,如图4所示。两者不同的是,在等待数据达到期间,I/O复用是会阻塞应用程序,而SIGIO方式是不会阻塞应用程序的。

![img](../assets/------1.png)

> ```
>                 图 4 信号驱动I/O模型示意图
> ```

### 异步I/O

POSIX规范定义了一组异步操作I/O的接口,不用关心fd 是阻塞还是非阻塞,异步I/O是由内核接管应用层对fd的I/O操作。异步I/O向应用层通知I/O操作完成的事件,这与前面介绍的I/O 复用模型、SIGIO模型通知事件就绪的方式明显不同。以aio_read 实现异步读取IO数据为例,如图5所示,在等待I/O操作完成期间,不会阻塞应用程序。

![img](../assets/--io-1.png)

> ```
>                  图 5 异步I/O 模型示意图
> ```

### I/O模型对比

前面介绍的5中I/O中,I/O 阻塞、I/O非阻塞、I/O复用、SIGIO 都会在不同程度上阻塞应用程序,而只有异步I/O模型在整个操作期间都不会阻塞应用程序。

如图6所示,列出了5种I/O模型的比较

![img](../assets/-------1--1.png)

> ```
>               图6 五种I/O 模型比较示意图
> ```

## 事件处理模型

网络设计模式中,如何处理各种I/O事件是其非常重要的一部分,Reactor 和Proactor两种事件处理模型应运而生。上章节提到将I/O分为同步I/O 和 异步I/O,可以使用同步I/O实现Reactor模型,使用异步I/O实现Proactor模型。

本章节将介绍Reactor和Proactor两种模型,最后将介绍一种使用同步I/O模拟Proactor事件处理模型。

### **Reactor事件处理模型** Reactor模型是同步I/O事件处理的一种常见模型,关于Reactor模型结构的资料非常多,一个典型的Reactor模型类图结构如图7所示

![img](../assets/import5.png)

> ```
>               图 7 Reactor 模型类结构图
> ``` **Reactor的核心思想** :将关注的I/O事件注册到多路复用器上,一旦有I/O事件触发,将事件分发到事件处理器中,执行就绪I/O事件对应的处理函数中。模型中有三个重要的组件:
> ```

- **多路复用器** :由操作系统提供接口,Linux提供的I/O复用接口有select、poll、epoll;
- **事件分离器** :将多路复用器返回的就绪事件分发到事件处理器中;
- **事件处理器** :处理就绪事件处理函数。
  图7所示,Reactor 类结构中包含有如下角色。
- **Handle** :标示文件描述符;
- **Event Demultiplexer** :执行多路事件分解操作,对操作系统内核实现I/O复用接口的封装;用于阻塞等待发生在句柄集合上的一个或多个事件(如select/poll/epoll);
- **Event Handler** :事件处理接口;
- **Event Handler A(B)** :实现应用程序所提供的特定事件处理逻辑;
- **Reactor** :反应器,定义一个接口,实现以下功能:

a)供应用程序注册和删除关注的事件句柄; b)运行事件处理循环; c)等待的就绪事件触发,分发事件到之前注册的回调函数上处理.

接下来介绍Reactor的工作流程如图8所示为Reactor模型工作的简化流程
![img](../assets/-------4-.png)

> ```
>              图8 Reactor模型简化流程示意图
> ```

1. 注册I/O就绪事件处理器
1. 事件分离器等待I/O就绪事件
1. I/O事件触发激活事件分离器分离器调度对应的事件处理器
1. 事件处理器完成I/O操作处理数据.
   网络设计中Reactor使用非常广在开源社区有很许多非常成熟的跨平台的Reactor模型的网络库比较典型如libevent

### Proactor事件处理模型

与Reactor不同的是Proactor使用异步I/O系统接口将I/O操作托管给操作系统Proactor模型中分发处理异步I/O完成事件并调用相应的事件处理接口来处理业务逻辑Proactor类结构如图9所示
![img](../assets/import8.png)

> ```
>                图9 Proactor模型类结构图
> ```

图9所示Proactor类结构中包含有如下角色

- **Handle**  用来标识socket连接或是打开文件
- **Async Operation Processor** 异步操作处理器负责执行异步操作一般由操作系统内核实现
- **Async Operation** 异步操作
- **Completion Event Queue** 完成事件队列异步操作完成的结果放到队列中等待后续使用
- **Proactor** 主动器为应用程序进程提供事件循环从完成事件队列中取出异步操作的结果分发调用相应的后续处理逻辑
- **Completion Handler** 完成事件接口一般是由回调函数组成的接口
- **Completion Handler A(B)** 完成事件处理逻辑实现接口定义特定的应用处理逻辑
  Proactor模型的简化的工作流程,如图10所示
  ![img](../assets/-------3-.png)

> ```
>             图10 Proactor模型简化工作流程示意图
> ```

1. 发起I/O异步操作注册I/O完成事件处理器;
1. 事件分离器等待I/O操作完成事件
1. 内核并行执行实际的I/O操作并将结果数据存入用户自定义缓 冲区
1. 内核完成I/O操作通知事件分离器事件分离器调度对应的事件处理器
1. 事件处理器处理用户自定义缓冲区中的数据
   Proactor利用异步I/O并行能力可给应用程序带来更高的效率但是同时也增加了编程的复杂度windows对异步I/O提供了非常好的支持常用Proactor的模型实现服务器而Linux对异步I/O操作(aio接口)的支持并不是特别理想而且不能直接处理accept因此Linux平台上还是以Reactor模型为主
   Boost asio采用的是Proactor模型但是Linux上采用I/O复用的方式来模拟Proactor另启用线程来完成读写操作和调度

### 同步I/O模拟Proactor

下面一种使用同步I/O模拟Proactor的方案原理是 **主线程执行数据读写操作读写操作完成后主线程向工作线程通知I/O操作完成事件”;** 工作流程如图 11所示
![img](../assets/11.png)

> ```
>              图11 同步I/O模拟Proactor模型
> ```

简单的描述一下图11 的执行流程

1. 主线程往系统I/O复用中注册文件描述符fd上的读就绪事件
1. 主线程调用调用系统I/O复用接口等待文件描述符fd上有数据可读
1. 当fd上有数据可读时通知主线程主线程循环读取fd上的数据直到没有更多数据可读然后将读取到的数据封装成一个请求对象并插入请求队列
1. 睡眠在请求队列上的某个工作线程被唤醒它获得请求对象并处理客户请求然后向I/O复用中注册fd上的写就绪事件主线程进入事件等待循环等待fd可写

## 并发模式

在I/O密集型的程序采用并发方式可以提高CPU的使用率可采用多进程和多线程两种方式实现并发当前有高效的两种并发模式半同步/半异步模式Follower/Leader模式

### 半同步/半异步模式

首先区分一个概念并发模式中的同步”、“异步 I/O模型中的同步”、“异步是两个不同的概念 **并发模式中**,“同步指程序按照代码顺序执行,“异步指程序依赖事件驱动如图12 所示并发模式的同步执行和异步执行的读操作 **I/O模型中**,“同步”、“异步用来区分I/O操作的方式是主动通过I/O操作拿到结果还是由内核异步的返回操作结果
![img](../assets/12.png)

> ```
>                 图12(a)  同步读操作示意图 
> ```

![img](../assets/d4293cab-d80d-4396-9790-425c1e414cc5.png)

> ```
>                 图12(b) 异步读操作示意图 
> ```

本节从最简单的半同步/半异步模式的工作流程出发并结合事件处理模型介绍两种演变的模式

#### **半同步/半异步工作流程**

半同步/半异步模式的工作流程如图13 所示
![img](../assets/13-1.png)

> ```
>          图13 半同步/半异步模式的工作流程示意图
> ```

其中异步线程处理I/O事件同步线程处理请求对象简单的来说

1. 异步线程监听到事件后将其封装为请求对象插入到请求队列中
1. 请求队列有新的请求对象通知同步线程获取请求对象
1. 同步线程处理请求对象实现业务逻辑

#### 半同步/半反应堆模式

考虑将两种事件处理模型即Reactor和Proactor与几种I/O模型结合在一起那么半同步/半异步模式就演变为半同步半反应堆模式先看看使用Reactor的方式如图14 所示
![img](../assets/15.png)

> ```
>                图14 半同步半反应堆模式示意图
> ```

其工作流程为

1. 异步线程监听所有fd上的I/O事件若监听socket接可读接受新的连接并监听该连接上的读写事件
1. 若连接socket上有读写事件发生异步线程将该连接socket插入请求队列中
1. 同步线程被唤醒并接管连接socket从socket上读取请求和发送应答
   若将Reactor替换为Proactor那么其工作流程为
1. 异步线程完成I/O操作并I/O操作的结果封装为任务对象插入请求队列中
1. 请求队列通知同步线程处理任务
1. 同步线程执行任务处理逻辑

#### 一种高效的演变模式

半同步半反应堆模式有明显的缺点

1. 异步线程和同步线程共享队列需要保护存在资源竞争
1. 工作线程同一时间只能处理一个任务任务处理量很大或者任务处理存在一定的阻塞时任务队列将会堆积任务的时效性也等不到保证不能简单地考虑增加工作线程来处理该问题线程数达到一定的程度工作线程的切换也将白白消耗大量的CPU资源
   下面介绍一种改进的方式如图15 所示每个工作线程都有自己的事件循环能同时独立处理多个用户连接
   ![img](../assets/16.png)

> ```
>             15 半同步半反应堆模式的演变模式
> ```

其工作流程为

1. 主线程实现连接监听只处理网络I/O连接事件
1. 新的连接socket分发至工作线程中这个socket上的I/O事件都由该工作线程处理工作线程都可以处理多个socket 的I/O事件
1. 工作线程独立维护自己的事件循环监听不同连接socket的I/O事件

### Follower/Leader 模式

Follower/Leader是多个工作线程轮流进行事件监听事件分发处理事件的模式
在Follower/Leader模式工作的任何一个时间点只有一个工作线程处理成为Leader 负责I/O事件监听而其他线程都是Follower并等待成为Leader
Follower/Leader模式的工作流概述如下

1. 当前Leader Thread1监听到就绪事件后从Follower 线程集中推选出 Thread 2成为新的Leader
1. 新的Leader Thread2 继续事件I/O监听
1. Thread1继续处理I/O就绪事件执行完后加入到Follower 线程集中等待成为Leader
   从上描述Leader/Follower模式的工作线程存在三种状态工作线程同一时间只能处于一种状态这三种状态为

- Leader线程处于领导者状态负责监听I/O事件
- Processing线程处理就绪I/O事件
- Follower等待成为新的领导者或者可能被当前Leader指定处理就绪事件
  Leader监听到I/O就绪事件后有两种处理方式:

1. 推选出新的Leader后并转移到Processing处理该I/O就绪事件
1. 指定其他Follower 线程处理该I/O就绪事件此时保持Leader状态不变
   如图16所示为上面描述的三种状态的转移关系
   ![img](../assets/17.png)

> ```
>          图16 Follower/Leader模式状态转移示意图
> ```

如图16所示处于Processing状态的线程处理完I/O事件后若当前不存在Leader就自动提升为Leader否则转变Follower
从以上描述中可知Follower/Leader模式中不需要在线程间传递数据线程间也不存在共享资源但很明显Follower/Leader 仅支持一个事件处理源集无法做到图15所示的每个工作线程独立监听I/O事件

## Swoole 网络模型分析

Swoole为PHP提供I/O扩展功能支持异步I/O同步I/O并发通信并且为PHP多进程模式提供了并发数据结构和IPC通信机制Swoole 既可以充当网络I/O服务器也支持I/O客户端较大程度为用户简化了网络I/O多进程多线程并发编程的工作
Swoole作为server时支持3种运行模式分别是多进程模式多线程模式多进程多线程模式多进程多线程模式是其中最为复杂的方式其他两种方式可以认为是其特例
本节结合之前介绍几种事件处理模型并发模式来分析Swoole server的多进程多线程模型如图17。![img](../assets/2.png)

> ```
>         图17 swoole server多进程多线程模型结构示意图
> ```

图17所示整体上可以分为Master ProcessManger ProcessWork Process Pool三部分这三部分的主要功能

1. **Master Process** 监听服务端口接收用户连接收发连接数据依靠reactor模型驱动
1. **Manager Process** Master Process的子进程负责fork WorkProcess并监控Work Process的运行状态
1. **Work Process Pool** 工作进程池与PHP业务层交互将客户端数据或者事件如连接关闭回调给业务层并将业务层的响应数据或者操作如主动关闭连接交给Master Process处理工作进程依靠reactor模型驱动
   Manager Process 监控Work Process进程本节不做进一步讲解主要关注Master和Work

#### **Master Process** Master Process 内部包括主线程(Main Thread)和工作线程池(Work Thread Pool),这两部分主要功能分别是: **主线程:** 监听服务端口,接收网络连接,将成功建立的连接分发到线程池中;依赖reactor模型驱动; **工作线程池:** 独立管理连接,收发网络数据;依赖Reactor事件处理驱动

顾一下前面介绍的半同步/半异步并发模式很明显主进程的工作方式就是图15所示的方式

#### Work Process

如上所描述Work Process是Master Process和PHP层之间的媒介

1. Work Process接收来自Master Process的数据包括网络数据和连接事件回调至PHP业务层
1. 将来自PHP层的数据和连接控制信息发送给Master Process进程Master Process来处理
   Work Process同样是依赖Reactor事件模型驱动其工作方式一个典型的Reactor模式
   Work Process作为Master Process和PHP层之间的媒介将数据收发操作和数据处理分离开来即使PHP层因消息处理将Work进程阻塞一段时间也不会对其他连接有影响
   从整体层面来看Master Process实现对连接socket上数据的I/O操作这个过程对于Work Process是异步的结合图11 所描述的同步I/O模拟Proactor模式两种方式如出一辙只不过这里使用的是多进程

#### **进程间通信** Work Process是Master Process和PHP层之间的媒介,那么需要看看Work Process 与Master Process之间的通信方式,并在Swoole server 的多进程+多线程模型进程中,整个过程还是有些复杂,下面说明一下该流程,如图18所示。 ![img](../assets/-------12--2.png)

> ```
>            图18 swoole server 多进程多线程通信示意图
> ```

具体流程为

1. Master 进程主线程接收客户端连接连接建立成功后分发至工作线程工作线程通过Unix Socket通知Work进程连接信息
1. Work 进程将连接信息回调至PHP业务层
1. Maser 进程中的工作线程接收客户端请求消息并通过Unix Socket方式发送到Work进程
1. Work 进程将请求消息回调至PHP业务层
1. PHP业务层构造回复消息通过Work进程发送Work进程将回复消息拷贝至共享内存中并通过Unix Socket通知发送至Master进程的工作线程有数据需要发送
1. 工作线程从共享内存中取出需发送的数据并发送至客户端
1. 客户端断开连接工作线程将连接断开的事件通过UnixSocket发送至Work进程
1. Work进程将连接断开事件回调至PHP业务层.
   需要注意在步骤5中Work进程通知Master进程有数据需要发送不是将数据直接发送给Master进程而是将数据地址(在共享内存中)发送给Master进程

## 改善性能的方法

性能对于服务器而言是非常敏感和重要的当前硬件的发展虽然不是服务器性能的瓶颈作为软件开发人员还是应该考虑在软件层面来上改善服务性能好的网络模块除了稳定性还有非常多的细节技巧处理来提升服务性能感兴趣的同学可以深入了解Ngnix源码的细节以及陈硕的Linux多线程服务器编程》。

### 数据复制

如果应用程序不关心数据的内容就没有必要将数据拷贝到应用缓冲区可以借助内核接口直接将数据拷贝到内核缓冲区处理如在提供文件下载服务时不需要将文件内容先读到应用缓冲区在调用send接口发送出去可以直接使用sendfile 零拷贝接口直接发送出去
应用程序的工作模块之间也应该避免数据拷贝

1. 当两个工作进程之间需要传递数据可以考虑使用共享内存的方式实现数据共享
1. 在流媒体的应用中对帧数据的非必要拷贝会对程序性能的影响特备是在嵌入式环境中影响非常明显通常采用的办法是给每帧数据分配内存下面统称为buffer),当需要使用该buffer时会增加该buffer的引用计数buffer的引用计数为0时才会释放对应的内存这种方式适合在进程内数据无拷贝传递并且不会给释放buffer带来困扰

### 资源池

在服务运行期间需要使用系统调用为用户分配资源通常系统资源的分配都是比较耗时的如动态创建进程线程可以考虑在服务启动时预先分配资源即创建资源池当需要资源从资源池中获取即可若资源池不够用时再动态的分配使用完成后交还到资源池中这实际上是用空间换取时间在服务运行期间可以节省非必要的资源创建过程需要注意的是使用资源池还需要根据业务和硬件环境对资源池的大小进行限制
资源池是一个抽象的概念常见的包括进程池线程池 内存池连接池这些资源池的相关资料非常多这里就不一一介绍了

### 锁/上下文切换

1. **关于锁** 对共享资源的操作是并发程序中经常被提起的一个话题都知道在业务逻辑上无法保证同步操作共享资源时需要对共享资源加锁保护但是锁不仅不能处理任何业务逻辑而且还存在一定的系统开销并且对锁的不恰当使用可能成为服务期性能的瓶颈
针对锁的使用有如下建议

1. 如果能够在设计层面避免共享资源竞争就可以避免锁如图15描述的模式;
1. 若无法避免对共享资源的竞争优先考虑使用无锁队列的方式实现共享资源
1. 使用锁时优先考虑使用读写锁此外锁的范围也要考虑尽量较少锁的颗粒度避免其他线程无谓的等待
   2. **上下文切换** 并发程序需要考虑上下文切换的问题内核调度线程(进程)执行是存在系统开销的若线程(进程)调度占用CPU的时间比重过大那处理业务逻辑占用的CPU时间就会不足在项目中线程(进程)数量越多上下文切换会很频繁因此是不建议为每个用户连接创建一个线程如图15所示的并发模式一个线程可同时处理多个用户连接是比较合理的解决方案
   多核的机器上并发程序的不同线程可以运行在不同的CPU上只要线程数量不大于CPU数目上下文切换不会有什么问题在实际的并发网络模块中线程(进程)的个数也是根据CPU数目来确定的在多核机器上可以设置CPU亲和性将进程线程与CPU绑定提高CPU cache的命中率建好内存访问损耗

### 有限状态机器

有限状态机是一种高效的逻辑处理方式在网络协议处理中应用非常广泛最典型的是内核协议栈中TCP状态转移有限状态机中每种类型对应执行逻辑单元的状态对逻辑事务的处理非常有效 有限状态机包括两种一种是每个状态都是相互独立的状态间不存在转移另一种就是状态间存在转移有限状态机比较容易理解下面给出两种有限状态机的示例代码**不存在状态转移** `c typedef enum _tag_state_enum{       A_STATE,     B_STATE,     C_STATE,     D_STATE }state_enum; void STATE_MACHINE_HANDLER(state_enum cur_state) {     switch (cur_state){     case A_STATE:          process_A_STATE();          break;     case B_STATE:          process_B_STATE();          break;     case C_STATE:          process_C_STATE();          break;     default:          break;     }     return ; }` **存在状态转移** ```c
void TRANS_STATE_MACHINE_HANDLER(state_enum cur_state) {
while (C_STATE != cur_state) {
switch (cur_state) {
case A_STATE:
process_A_STATE();
cur_state = B_STATE;
break;
case B_STATE:
process_B_STATE();
cur_state = C_STATE;
break;
case C_STATE:
process_C_STATE();
cur_state = D_STATE;
break;
default:
return ;
}
}
return ;
}

时间轮

经常会面临一些业务定时超时的需求,用例子来说明吧。功能需求 :服务器需要维护来自大量客户端的TCP连接(假设单机服务器需要支持的最大TCP连接数在10W级别),如果某连接上60s内没有数据到达,就认为相应的客户端下线。

先介绍一下两种容易想到的解决方案,方案a 轮询扫描 处理过程为:

  1. 维护一个map 记录客户端最近一次的请求时间;
  2. 当client_id对应连接有数据到达时,更新last_update_time;
  3. 启动一个定时器,轮询扫描map 中client_id 对应的last_update_time,若超过 60s,则认为对应的客户端下线。

轮询扫描,只启动一个定时器,但轮询效率低,特别是服务器维护的连接数很大时,部分连接超时事件得不到及时处理。方案b 多定时器触发 处理过程为:

  1. 维护一个map 记录客户端最近一次的请求时间;
  2. 当某client_id 对应连接有数据到达时,更新last_update_time,同时为client_id启用一个定时器,60s后触发;
  3. 当client_id对应的定时器触发后,查看map中client_id对应的last_update_time是否超过60s,若超时则认为对应客户端下线。

多定时器触发,每次请求都要启动一个定时器,可以想象,消息请求非常频繁是,定时器的数量将会很庞大,消耗大量的系统资源。方案c 时间轮方案 下面介绍一下利用时间轮的方式实现的一种高效、能批量的处理方案,先说一下需要的数据结构:

  1. 创建0~60的数据,构成环形队列time_wheel,current_index维护环形队列的当前游标,如图19所示;
  2. 数组元素是slot 结构,slot是一个set,构成任务集;
  3. 维护一个map,记录client_id 落在哪个slot上。

img

>                 图19 时间轮环形队列示意图
> ```

执行过程为:

1.  启用一个定时器,运行间隔1s,更新current\_index,指向环形队列下一个元素,0->1->2->3...->58->59->60...0;
2.  连接上数据到达时,从map中获取client\_id所在的slot,在slot的set中删除该client\_id;
3.  将client\_id加入到current\_index - 1锁标记的slot中;
4.  更新map中client\_id 为current\_id-1 。**与a、b两种方案相比,方案c具有如下优势** :

1.  只需要一个定时器,运行间隔1s,CPU消耗非常少;
2.  current\_index 所标记的slot中的set不为空时,set中的所有client\_id对应的客户端均认为下线,即批量超时。

上面描述的时间轮处理方式会存在1s以内的误差,若考虑实时性,可以提高定时器的运行间隔,另外该方案可以根据实际业务需求扩展到应用中。我们对Swoole的修改中,包括对定时器进行了重构,其中超时定时器采用的就是如上所描述的时间轮方案,并且精度可控。