系统学习-协程理解


在面对异步IO频繁的业务需求的时,可以使用回调的机制。在利用回调的过程中,如果利用状态机则会发生回调金字塔(callback hell),主要表现为:1.代码复用率极低。2.逻辑复杂。此时利用协同程序可以很好的解决这个问题。

CPU (CentralProcessingUnit)

计算机的核心是CPU,所有的计算任务都是由它完成。CPU概念包括:

物理CPU

物理CPU就是插在主机上的真实的CPU硬件,在Linux下可以数不同的physical id 来确认主机的物理CPU个数。

核心数

物理CPU下一层概念就是核心数,我们常常会听说多核处理器,其中的核指的就是核心数。在Linux下可以通过cores来确认主机的物理CPU的核心数。

逻辑CPU

逻辑CPU跟超线程技术有联系,假如物理CPU不支持超线程的,那么逻辑CPU的数量等于核心数的数量;如果物理CPU支持超线程,那么逻辑CPU的数目是核心数数目的两倍。在Linux下可以通过 processors 的数目来确认逻辑CPU的数量。

层级如下图所示:

多核与多线程

进程与线程

**进程是CPU资源分配的最小单位,线程是CPU调度的最小单位。**进程包含线程,常见的模型有:

1.单进程单线程模型

进程与线程的区别在于:进程掌管着资源,线程是进程的一部分,CPU执行调度的是线程。

2.单进程多线程模型

多个线程共享全局资源,但不包括线程自己的栈空间。

3.多进程单线程模型

单个应用可以通过fork拷贝出多个进程,进程的资源会进行复制。注意:fd同样会复制,不建议多个进程共用同一个fd,会出现数据乱序的情况。

4.多进程单线程模型

涉及多线程时,锁是个必须掌握的知识点,否则同时对共享资源进行处理可能导致覆盖写问题。

网络编程中5种I/O模型

在Unix(linux)平台下有5中I/O模型:
同步I/O模型

堵塞I/O模型(blocking I/O)
非堵塞I/O模型(un-blocking I/O)
I/O多路复用模型(select, poll, epoll)(较常见)
信号驱动I/O模型(SIGIO)

异步I/O模型
同步与异步的区别
同步:指关于这个I/O中的一系列动作都需要自己来完成,无论你是原地等待事件的发生(阻塞)还是当某个事件已经准备好的时候你去完成后面的的动作(非阻塞)都属于同步。
异步:它是指是调用另一个执行者去完成,当执行者发现要处理的事件后调用你,你再完成这件事情,执行的过程和你的动作是不牵扯的。
因此,前四种是同步I/O模型,只有第五种是异步的。

I/O操作一般分为两个阶段:

  • 等待数据达到内核缓存区
  • 将数据从内核拷贝到用户进程

阻塞型I/O

阻塞型I/O

通常把阻塞的文件描述符(file descriptor,fd)称之为阻塞I/O。默认条件下,创建的socket fd是阻塞的,针对阻塞I/O调用系统接口,可能因为等待的事件没有到达而被系统挂起,直到等待的事件触发调用接口才返回,例如,tcp socket的recvfrom调用会阻塞至连接有数据返回,如上图所示。另外socket 的系统API ,如,accept、send、connect等都可能被阻塞。

非阻塞型I/O

一种轮询的机制
非阻塞型I/O
把非阻塞的文件描述符称为非阻塞I/O。可以通过设置SOCK_NONBLOCK标记创建非阻塞的socket fd,或者使用fcntl将fd设置为非阻塞。
对非阻塞fd调用系统接口时,不需要等待事件发生而立即返回,事件没有发生,接口返回-1,此时需要通过errno的值来区分是否出错,有过网络编程的经验的应该都了解这点。不同的接口,立即返回时的errno值不尽相同,如,recv、send、accept errno通常被设置为EAGIN 或者EWOULDBLOCK,connect 则为EINPRO- GRESS 。

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事件以提高事件处理效率。

好处:其实这就是一个回调实现的机制。在这个过程中,只需要两个线程就可以完成多个连接请求。一个为业务线程,一个为epoll模型监听线程。这样的话,就可以利用一个业务线程进行大量的访问请求处理。而不必像PHP等实现机制,每一个请求都分配一个线程,之后阻塞等待。

回调机制

在这种典型的回调机制的实现过程中,通过epoll模型返回的结果即状态机的条件,以及结合上下文即状态机的现态,可以触发动作。即:条件+现态->动作(状态机是一种switch-case的结构,逻辑是非顺序的,类似于一种表格的结构—详见深入浅出理解有限状态机),因为状态机的逻辑非顺序化,所以将其逻辑顺序化的过程,会出现callback hell的问题。如下图所示:
回调金字塔-callback_hell

解决这样的问题协程是一种有效的手段,即:协程可以处理大量的异步I/O操作的需求业务。

信号驱动I/O

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

异步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操作完成期间,不会阻塞应用程序。

目前常见的服务端模型(多进程结合I/O多路复用)

目前常见的服务端模型
伪代码:

void dispatch(...){
    将connect_fd加入epoll队列,等待可读(可写)事件
}
void run(...){
    执行业务逻辑(read或者write该fd)
    //其他业务逻辑(如curl)
}
int main()
{
    创建listen_fd监听端口
    fork创建子进程
    if(当前进程为父进程){
        /*************父进程***************/
        管理子进程
    }else{
        /*************子进程***************/
        子进程创建epoll队列
        将dispatch事件绑定到listen_fd的可读事件上
        使用epoll函数监听listen_fd
        while(1){
            //第一次触发epoll函数
            出现listen_fd可读(可写)事件,触发dispatch事件
            epoll队列中有listen_fd和每个客户端的connect_fd
            //第N次(N!=1)
            出现listen_fd或者connect_fd的可读(可写)事件,触发对应的dispatch事件或者run事件

        }

    }
}

从服务器端的代码逻辑可以知道,epoll是可以同时监听多个fd的,并且在有对应的事件时才唤醒对应的事件函数,这是通常说的异步调用。
但是,这里有个问题,如果在run函数中业务逻辑需要创建tcp连接(如curl)请求其他服务接口(创建fd),此时的fd因为没有使用epoll,就会出现阻塞。
那我们设想一下,如果在run函数中写epoll监听该fd,那如果这个tcp连接请求的后续事件依旧需要创建tcp请求呢,这个代码该怎么编写下去呢,这就是常说的一种情况,回调地狱。
那如何优雅解决这个问题呢?
设想一下,如果我们不需要去写epoll监听,直接对read/write函数进行hook,默认所有IO操作行为会被直接加入epoll队列,这样就不会有回调地狱。因此,协程诞生了!

协程

协程是一种程序组件,是由子例程(过程、函数、例程、方法、子程序)的概念泛化而来的,子例程只有一个入口点且只返回一次,而协程允许多个入口点,可以在指定位置挂起和恢复执行。协程是一种“伪多线程”,一个线程中可以包含多个协程,但同一时刻只能有一个协程在运行。

协程是一种可以暂停执行过程的函数,它可以中断当前的执行过程直到下一个Yield指令达成。在实现上,大多数都是以函数来作为一个协程,因此这里列出此简化的定义方便理解。
例如:实现一个0到9的循环输出。

int function(void) {
  static int i, state = 0;
  switch (state) {
    case 0: /* start of function */
    for (i = 0; i < 10; i++) {
      state = __LINE__ + 2; /* so we will come back to "case __LINE__" */
      return i;
      case __LINE__:; /* resume control straight after the return */
    }
  }
}

用static变量保存上下文,再用switch进行代码行的跳转,就可以实现一个简单的协程。

协程的运用

协程模型

总结

协程主要适合于一些IO比较频繁的系统,在这样的系统中,使用协程跟多线程的优缺点比较如下:

  • 单线程异步IO: 优点是性能高,代码执行是顺序的,不需要关心锁,竞争等情况;缺点是需要自己处理异步I/O、epoll等,无法便捷地做到hook所有fd操作;
  • 协程: 比单线程异步I/O容易编程,代码更好写,协程里面是顺序编程的,但协程之间是独立栈,共享堆内存,单线程执行环境,在一个CPU上运行。协程切换代价比线程少多了,只需要十几条汇编指令切换寄存器。每秒据说能达到上百万次切换。
  • 多线程同步I/O: 代码相对也好写,跟协程一样独立栈,共享堆内存。 需要处理资源竞争问题,而且线程切换代价特别大,linux里面没有原生的线程,是用进程实现的。
  • 多进程:代码相对容易编写,但需要解决共享数据的问题。

猜你喜欢

转载自blog.csdn.net/BigBrick/article/details/85317447