前言之前的前言
本文作于6月中旬,当时对于很多概念不是很理解,所以写到一半实在进行不下去,通过最近的学习终于理解了一些,赶紧总结记下。
前言
本篇主要总结服务器端开发中的一些基本的框架。
如果你在东区二楼点过黄焖鸡,相信你一定能更好的理解。
正文
I/O模型
主要可以分为同步I/O,异步I/O 两大类。
同步I/O
我们可以理解为,在I/O事件发生后(出现了I/O请求),由应用程序负责处理I/O,或者说,内核向应用程序通知I/O就绪事件。
理论上来说,阻塞I/O(如read一直等待),I/O复用(如select,epoll),信号I/O都是同步I/O。
异步I/O
这俩当然是相反的咯,在I/O事件发生后(出现了I/O请求),由内核负责处理I/O,内核向应用程序通知I/O完成事件,比如Linux下的aio,(还有C++的asio??)
理解
之前对于同步和异步总是很混乱,看过之后才明白,同步I/O是由应用程序来处理,而异步只是应用程序将要做的I/O处理提前告诉了内核,一旦需要处理,内核直接按照之前的要求完成,然后将结果告诉应用程序。
举个例子:
康康到食堂点了一份黄焖鸡,
对于同步I/O,鸡做好了(I/O事件发生),小姐姐喊号(内核通知应用程序I/O就绪),康康端着鸡回去了(应用程序自己处理I/O)。
而对于异步I/O,康康在点鸡时就告诉了小姐姐,请送到FZ118(将请求告诉内核),鸡做好了(I/O事件发生),小姐姐直接端到FZ118(内核按照要求直接处理I/O),然后告诉康康,鸡送到了(内核通知应用程序I/O完成)。
同步/异步和阻塞/非阻塞的关系?
当初这两个也搞不明白,现在大致清楚了。
异步I/O自然是非阻塞,因为应用程序只需通知内核,这一事件不需要什么前提,程序没有阻塞。
同步I/O中的阻塞I/O,如read,没有数据则程序一直阻塞,自然是阻塞的。
同样,程序也会阻塞在I/O复用(如select时间内一直没有事件发生),但是对于发生的I/O事件,并不是阻塞的,一旦出现了I/O,直接处理。
信号I/O同理,出现事件直接处理。并且信号注册的过程也只是和通知一样,程序也不阻塞。
所以这俩货没啥直接对应关系。。。
事件处理模式
服务器端主要需要处理三类事件,I/O事件,定时事件和信号,首先从整体上分成两种事件处理模式。
Reactor — 同步I/O
主线程(I/O处理单元)负责监听socket上的I/O事件。一旦出现(如可读,有新的连接),则主线程将socket交给 工作线程去处理,然后自己继续监听,工作线程把各种活干完了,所以它并不区分读写线程
这里是在I/O出现之后,内核通知应用程序,工作线程最终处理I/O。自然是同步模型。
Proactor — 异步I/O
主线程通过epoll监听socket,一旦有连接socket则在内核注册读完成事件(告诉内核读到哪里,读完之后发个怎样的通知,这里以信号为例)。内核读好之后信号发给程序,通过信号通知工作线程进行业务逻辑处理,工作线程处理完业务逻辑后向内核注册写完成事件(写到哪里,写完之后发个怎样的通知)内核发送信号,程序再决定下一步操作。
并发模式
并发模式是指I/O处理单元和逻辑单元协调完成任务的方法。
对于计算密集型的程序,并发编程没有什么优势(反而由于进程间的切换降低了效率),但是对于I/O密集型的程序,通过进程的调度减少了CPU等待的事件提升了效率。
在下文中,我们提到的同步和异步 是指代码的执行顺序,同步是顺序执行,而异步是程序执行由事件驱动。
半同步-半异步模式(half-sync/half-async)
顾名思义,既有同步线程,又有异步线程。
同步线程负责处理客户逻辑,而异步线程负责处理I/O事件。
最简单的一个例子,和生产者–消费者模型类似,异步线程监听socket,一旦有请求出现,将其封装成请求对象,放到消息队列中,然后队列将请求分发给同步线程,根据需求处理。
一种变体—半同步-半反应堆模式(half-sync/ half-reactive)
主线程为异步线程,负责监听socket,若有连接请求,则将其在epoll中注册读写事件,如果已经连接的socket上有读写事件发生,则把其插入到请求队列中。
而其余的同步线程,则通过竞争的方式从队列中获取socket,再进行读写和业务逻辑的处理。
这里是一个典型的reactor模式哦,复习一下上面的reactor模式(主线程只负责监听,活都给工作线程了)
我觉得这就是一个半同步半异步线程池,异步指接收到socket、放入队列,而读写socket,处理业务逻辑则是同步的。
缺点主要有两点:
一、这其实就是一个典型的生产者—消费者的例子,那么对于这个队列的操作我们需要加锁—一加锁自然效率下降(耗费CPU)。
二、每个工作(同步)线程一次只能处理一个客户请求,处理完才能换成下一个,那么对于突然的大量请求。。只能靠增加线程来解决,这里的问题就是大量线程的上下文切换又会是一步很大的开销。
在之后的博客中我会分享一个C++实现的线程池。
一种高效一点的半同步—半异步模式
主要是针对上面的两个缺点进行的改进。
首先消息队列被取消了,主线程在监听socket上得到连接socket后直接传递给工作线程,而线程则将其注册到自己的epoll内核事件表中,然后epoll_wait等待处理上面发生的读写事件。
这里消息队列取消,我们不再需要加锁,提高效率。而且每个工作线程可以处理多个客户请求。
(当然,由于每个工作线程现在也是epoll事件驱动了。。感觉不是同步线程了)
领导者-追随者模式(Leader-Follower)
这里的Leader就是负责监听socket的线程,不同的是当监听到事件发生,Leader可以自己直接去进行处理(或者指派给Follower)
在这种模式中,线程一共有三种状态,Leader、Follower、Processing。
一共有三种组件:句柄集、线程集、事件处理器。
句柄集就是监听很多句柄(fd/socket)发生事件时通知给Leader。
每时刻只能有一个Leader等待I/O事件通知,可以有多个Follower等待工作。
然后Leader可以选择自己通过事件处理器处理事件,也可以指派Follwer去处理。
事件处理器是各种绑定在句柄上的回调函数,所以线程处理事件是直接执行回调函数。
处理的线程为Processing状态。
线程集就是调度所有线程,负责线程之间的同步,选举Leader线程。
如果Leader自己去处理I/O,还要通过线程集去选举下一个Leader。
当Processing处理完之后,如果没有Leader则成为Leader否则是Follower。
与上一种高效半同步–半异步相比,Leader直接处理请求省去了线程之间的消息传递,但是缺点是一个线程依然只能处理一个客户请求。。。