β

事件驱动与协程概念

Harries Blog™ 402 阅读

事件驱动与协程概念

在一个完美的世界中,不存在 战争 和饥饿,所有的 API 都将使用异步方式编写,兔兔和小羊羔将会在阳光明媚的绿色草地上手牵手地跳舞

IO 讲起

应用独占式

在计算机发展的初期,每个应用都是独占式的,没有OS进行调度,每次只加载一个 进程 ,学过单片机的朋友应该有过这样的体验,例如常用的8086系列芯片,我当时学习微机原理课程是使用仿真 软件 Proteus,写出汇编, 编译 成二级制文件,load的仿真软件上,就可以运行。通常我们写的汇编程序会控制一些基本外设,例如键盘,灯,蜂鸣器,定时器之类的,其中比较关键的就是中断,当外设被触发,会向CPU发出一个中断信号,CPU的中断处理机制,会保护好发起中断的现场,然后去执行中断处理函数的地址,处理完以后,会回到刚刚保存的现场。

由于单片机的只是我已经忘得差不多了,就拿一个完整的计算机结构图来说一说。假设是最原始的计算机,没有OS或者说我们的程序就是一个简单的OS,程序完成的工作是:

1.用户输入hello

2.从磁盘中读取world

3.组合成 hello world并写入磁盘

1和2这个两个任务其实是没有先后顺序的,但是在一个进程的世界里,必须要有先后顺序,并不能并发执行,于是整个执行流程就是这样:CPU进入中断,程序等待用户输入,用户输入之后,CPU中断返回,将IO总线上拿到的hello的字节写入主存的某个地址,接着发送指令读取磁盘的world的地址并等待字节返回,然后写入主存的hello的前一个地址,然后发送指令将hello world对应地址的内容写入磁盘。

事件驱动与协程概念

OS协调式-为了并发

由于硬件发展,只有一个程序在CPU上跑有些浪费,于是OS作为一个大 管家 来协调大家的资源 需求 ,于是抽象了进程的概念,当多个进程并发在一个CPU上跑时,一个被IO阻塞,OS可以让CPU执行别的进程。 事件驱动与协程概念

后来由于进程切换比较消耗CPU,并且也不能资源共享,于是抽象出 线程 ,线程的CPU使用也是由OS协调,OS通过 时间 片的方法进行强占式CPU资源分配,程序的编写者不用关注什么时候让出资源,什么时候执行 代码 ,全都由OS 管理 ,这时看起来已经很完美了,世界一片明亮。 事件驱动与协程概念

高并发 下的挑战

有了线程之后,我们处理并发最直观的做法就是加线程,为了减少线程的启动时间,我们开始使用 线程池 ,预先启动一些线程。随着并发进一步提高,加上外部请求基本上都是IO密集型,使用线程带来的效益开始下降,也就是说在线程的 生命 周期中IO等待时间远远大于CPU计算时间,另外每个线程大约需要4M的内存,由于内存的限制,单机线程数不会很多。所以初期的Apache、 tomcat 服务器 通常只能处理几千的并发。为了 突破 单机下的并发问题,以nginx为首的一种叫事件驱动的方案开始流行。

为了代码好看、好写

事件驱动其实充分利用了线程,对于有阻塞的操作,就扔过去一个回调,主流程继续执行,当阻塞的流程执行完成就会调用回调函数。这种异步的方法与之前的 同步 写法不一样,例如一件事需要1,2,3,4这样的顺序执行,假如这四个步骤都是阻塞的话,就需要三层回调,要是步骤再多一点就会产生回调地域,代码可读性很差,还容易写错。于是一些语言就出了第三方库就出来帮忙,声明出叫做协程的概念,可以用同步的方式写异步,例如c++的 lib go, java 的Quasar,还有一些新颖的语言,直接将这个特性加入官方库,例如go、 python 3、kotlin、java11

事件驱动

事件驱动的最初应用是在 UI 编程上,其中很重要的一点就是需要感知一些外设的操作,从 本质 上还是IO,我们的一次鼠标点击,键盘敲击,触摸屏的滑动都是一个事件,会放在OS的队列中,最初的做法就是专门有个线程去轮序各个队列看看有没有相关事件,但是这样比较浪费CPU资源,于是OS说你应用不要来不断问我了,你先来告诉我你关注哪些事件类型,等事件发生了我告诉你得了。于是应用的UI线程开开心心等着事件通知,不用再跑着去问OS了。之后这种 模型 在后端也发扬广大,下面我么来举两个栗子。

UI方面以 Android 为例,在应用启动时会有创建一个UI主线程,在主线程中会调用Looper.loop方法,该方法是一个死循环,用来更新UI,但是不会卡死,内部使用了 linux 的epoll机制。Andro id 应用程序的主线程在进入消息循环过程前,会在内部创建一个Linux管道(P ip e),这个管道的作用是使得Android应用程序主线程在 消息队列 为空时可以进入空闲等待状态,并且使得当应用程序的消息队列有消息需要处理时唤醒应用程序的主线程。在线程没有消息处理时,虽然有死循环,但是通过linux I/O阻塞机制让程处于空闲状态,有能力去执行其他操作,所以不会因为looper死循环导致线程卡死,当然主线程的UI也不会卡顿。

事件驱动与协程概念

后端方面以 Netty 为例, 有一个主线程对应bossEventLoopGroup中的唯一的一个EventLoop,其中也是一个循环,通过 NIO 的方式(在Linux上底层依然是使用Epoll)或者Epoll的方式,调用 操作系统 的阻塞方法等待事件到来,然后将事件放入WorkEventLoopGroup的队列中,等待EventLoop来执行。

事件驱动与协程概念

netty这个结构可能比较复杂,还是以处理网络连接为例,下图更简单的描述了事件驱动,用一个线程处理所有的连接,这个线程通常是一个循环的方法,当处理一个连接遇到阻塞的操作就将任务丢给其它的线程,主线程接着处理下一个连接,有没有感觉和Android的UI模型出奇的相似。

事件驱动与协程概念

对比上面的两个例子,UI主线程相当于netty中的那个bossEventLoop,同样适用epoll机制,通过系统调用的阻塞等待事件的到来,之后将事件分发出去,让相应的handler处理。

作者:Harries Blog™
追心中的海,逐世界的梦
原文地址:事件驱动与协程概念, 感谢原作者分享。