服务器的多任务并发处理(三)---两种高效的事件处理/并发模式

一、同步,异步,阻塞,非阻塞

(一)同步、异步

首先我们要知道同步I/O和异步I/O是针对应用程序和内核的交互而言的。

(1) 同步I/O是指用户代码自行执行I/O操作,将数据从内核缓冲区读入用户缓冲区,或从用户到内核。它需要等待或者轮询的去查看I/O操作是否就绪,返回给用户的是I/O就绪事件

(2) 异步I/O是指用户可以直接对I/O执行读写操作,这些操作告诉内核用户读写缓冲区的位置,以及I/O操作完成后内核通知应用程序的方式。异步I/O总是立即返回,不管I/O是否阻塞,因为真正的读写操作已经有内核结构。用户进程触发IO操作以后便开始做自己的事情,而当IO操作已经完成的时候会得到IO完成的通知。即异步I/O向用硬程序通知的是I/O完成事件

(二)阻塞、非阻塞

  1. 阻塞和非阻塞是针对于进程在访问数据的时候,根据IO操作的就绪状态来采取的不同方式。

(1) 阻塞I/O是系统调用可能因为无法立即完成而被操作系统挂起,直到等待的事件发生为止。在网络编程中,socket在创建的时候默认是阻塞的。但是我们可以通过fcntl系统调用去设置为非阻塞。阻塞和非阻塞的概念应用于所有文件描述符。如客户端通过connect向服务端发起连接时,connect三次握手先发送同步报文给服务器,等待服务端的确认报文,如果没到则connect被挂起,直到客户端收到确认报文,唤醒connect调用。socket的基础API中可能被阻塞的系统调用有:accept,send,recv,connect。我们在ET模式时将recv系统调用设置为了非阻塞,就是为了防止它数据接收完毕一直阻塞,不能进行下一次epoll唤醒。

(2) 非阻塞I/O就是执行的系统调用总是立即返回,而不管事件是否已经发生,如果事件没有立即发生,这些系统调用就立即返回-1,和出错的情况一样,这时我们必须根据errno全局变量来区分这两种情况。对于accept,send,recv,事件未发生时errno被设置为EAGAIN(再来一次),或为EWOULDBLOCK(期望阻塞),对于connect来说erron被设置为EINPROGRESS(在处理中)

(三)同步阻塞,同步非阻塞,异步阻塞,异步非阻塞I/O

一个I/O操作可以分为两个步骤:发起I/O请求和实际的I/O操作。

  • 阻塞I/O和非阻塞I/O的区别在于第一步,发起I/O请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞I/O,如果不阻塞,那么就是非阻塞I/O。
  • 同步I/O和异步I/O的区别就在于第二个步骤是否阻塞,如果实际的I/O读写阻塞请求进程,I/O操作完成后才能返回,那么就是同步I/O。如果不阻塞,而是操作系统做完IO两个阶段的操作再将结果返回给用户线程,那么就是异步IO。

一般来说I/O模型可以分为:同步阻塞,同步非阻塞,异步阻塞,异步非阻塞I/O。
(1)同步阻塞I/O:
用户进程在发起一个I/O操作以后,必须等待I/O操作的完成,只有当真正完成了I/O操作以后,用户进程才能运行。我们还是以快递派发员来说,这个就好比我一直在快递点等着(阻塞),时不时看一下有没有人来取快递(轮询同步)
(2)同步非阻塞I/O:
用户进程发起一个I/O操作以后边可返回做其它事情,但是用户进程需要时不时的询问I/O操作是否就绪,这就要求用户进程不停的去询问,从而引入不必要的CPU资源浪费。类似我在隔壁饭店看电视(非阻塞),时不时的到快递站点看一下有没有人来取快递(同步)。
(3)异步阻塞I/O:
此种方式下是发起一个I/O操作以后,然后等待I/O操作的完成,但是不需要等待内核I/O操作的完成,它可以直接去做自己的事,等内核完成I/O操作以后会通知应用程序,。类似我在快递站点一直等着监工(阻塞),我雇了一个人(内核),他每次去帮别人取,取好了告诉我就好(异步),而不是告诉我有人取快递。
(4)异步非阻塞I/O:
在此种模式下,用户进程只需要发起一个I/O操作然后立即返回,等I/O操作真正的完成以后,应用程序会得到I/O操作完成的通知,此时用户进程只需要对数据进行处理就好了,不需要进行实际的I/O读写操作,因为真正的I/O读取或者写入操作已经由内核完成了。类似我在隔壁饭店看电视(非阻塞),雇了一个人,帮别人取好快递告诉我就好(异步)

二、两种高效的事件处理模式

(一)Reactor 模式

通常使用同步I/O模型实现。它要求主线程只负责监听文件描述符上是否有事件发生,有的话就立即将该事件通知给工作线程,除此之外,主线程不做任何其他实质性的工作。读写数据,接受新的连接,以及处理客户请求均在工作线程中完成。、
通常使用同步I/O模型实现,例如epoll,工作模式是:
(1) 主线程往epoll内核事件表中注册socket上的读就绪事件
(2) 主线程调用epoll_wait()等待socket上有数据可读。
(3) 当socket上有数据可读时,epoll_wait()通知主线程,主线程将socket可读事件放入请求队列。
(4) 睡眠在请求队列上的某个工作线程被唤醒,它从socket读取数据,并处理客户请求,然后往epoll内核事件表上注册该socket上的写就绪事件。
(5) 主线程调用epoll_wait()等待socket可写
(6) 当socket可写时,epoll_wait()通知主线程,主线程将socket()可写事件放入请求队列。
(7) 当睡眠在请求队列上的某个工作线程被唤醒,它往socket上写入服务器处理客户请求的结果
服务器的多任务并发处理(三)---两种高效的事件处理/并发模式
主线程就类似于催化剂,它只负责接受,逻辑处理都在线程中处理。

我们前面实现的线程池加上I/O复用就是这种事件处理模式的。这种模式适用于I/O密集型,即客户端发送数据和服务端回复数据多的情况,原因在这种模式是把读写操作放在工作线程处理,不会让主线程有很大的压力。

(二)Proactor模式

工作原理是将所有的I/O操作都交给主线程和内核来处理,工作线程仅仅负责业务逻辑。适用于业务类型的,即I/O连接少,业务处理复杂。

三、两种高效的事件处理模式

(一)半同步/半异步模式

这里说的同步异步和我们在I/O模型里面讨论的不一样:

  • 在I/O模型里,同步异步区分的是内核向应用程序通知的是何种I/O事件,是就绪事件还是完成事件,以及该由谁来完成I/O读写。
  • 在并发模式中,同步是指程序安全按照代码序列的顺序执行,异步是指程序的执行需要有系统事件来驱动,包括中断,信号等。
    服务器的多任务并发处理(三)---两种高效的事件处理/并发模式

同步方式运行的我们就称为同步线程,异步方式就称为异步线程。
优缺点:异步线程执行效率高,实时性强,编写复杂,难以调试,适用于嵌入式程序,不适合大量的并发。同步效率低,实时性差,但逻辑简单。故为了针对像服务器这种既要求较好的实时性,又要求能同时处理多个客户端的应用程序,我们需要采用半同步/半异步模式来实现。
半同步/半异步时,同步线程用于处理客户逻辑,异步线程用于处理I/O事件。异步线程监听到客户请求,通知工作线程在同步方式下处理事件。
下面我们介绍两种常用的模型。

1、半同步/半反应堆模型

服务器的多任务并发处理(三)---两种高效的事件处理/并发模式
主线程以异步的方式工作,称为异步线程,它负责监听所有socket上的事件,如果socket上有可读事件发生,即有新的连接请求到来,主线程接受它得到新的连接socket,然后往epoll内核事件表中注册该socket上的读写事件。如果连接socket上有读写事件发生,即有新的客户请求到来或有数据要发送至客户端,表明事件就绪了,主线程就将该连接的socket插入请求队列中,所有的工作线程都睡眠在请求队列上,当有任务到来时,它们将通过竞争获得任务接管权。这种竞争机制使得只有空闲的工作线程才有机会来处理新任务。
上图中工作线程需要自己从socket上读取客户请求和往socket写入服务器应答,这就表示主线程只负责监听,工作线程需要负责数据处理,逻辑处理,这就是典型的Reactor 模式。
缺点:
(1) 主线程和工作线程共享请求队列,主线程往请求队列中添加任务,工作线程从请求队列中取出任务,所以需要对请求队列进行加锁保护,这个浪费了CPU时间。
(2) 每个工作线程在同一时间只能处理一个客户请求,如果客户数量较多,而工作线程较少,那么请求队列就会堆积很多任务,客户端的响应速度会越来越慢,如果增加线程来解决这一问题,则工作线程的切换也会耗费大量的CPU时间。
这时一种高效的半同步/半异步模式出现了,它的每个工作线程都能同时处理多个客户端连接。

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

服务器的多任务并发处理(三)---两种高效的事件处理/并发模式
主线程只管监听socket,连接socket由工作线程来管理,当有新的连接到来时,主线程就接受并派发给某个工作线程,此后这个socket上的任何I/O操作都有被选中的工作线程负责,直到客户端关闭连接,主线程向工作线程派发socket的最简单的方式,就是往它和工作线程之间的管道写数据,工作线程检测到管道上有数据可读时,就分析是否有一个新的客户连接到来,如果是,则把该新socket上的读写事件注册到自己的epoll内核事件表上。所以每个线程都有自己的事件循环,它们各自独立监听不同的事件。

总结:

  • 半同步/半反应堆模型的工作原理就好比:我是快递员(主线程),把所有的物品(新的连接)都登记在小册子上(内核事件表),当物品A的主人A来取件时(就绪事件),我就把物品A的信息放在就绪册子上(请求队列),我就派一个人(工作线程)先从就绪册子(请求队列)上获取到A物品的信息,然后去给主人A取物品(处理数据)。
  • 高效的半同步/半异步模式的工作原理就好比:我是快递员(主线程),每一个物品(新的连接)我分配给不同的人(工作线程),以后这个物品就由它的负责人负责了,负责人把物品记录在自己的小册子上(内核事件表),负责人人是随机选的,所以一个负责人会管理一个或多个物品。之后这个物品的取件(就绪)都由对应的负责人负责。

(二)Proactor模式

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

  • 例如:10个线程,A……J,最开始A为领导者,负责监听事件,其余9个为追随者。当检测到事件发生,A做两件事,首先先从9个追随者里选一个领导者,然后自己去处理事件,成为追随者。这时假如新的领导者为B,B线程就是领导者,继续检测,9个追随者,这样不断循环,实现了并发。

加油哦!????。