异步编程和事件驱动
一般我们学习的是传统的顺序编程, 所有发送给解释器的指令会一条条地被执行, 这样写出来的代码输出比较直观且可预测, 如果出了问题可以按照顺序执行下去即可发现问题, 可以比较好的开发和调试。
同步/异步
同步和异步描述的是进程/线程的调用方式。
同步调用指的是线程发起调用后, 一直等待调用返回后才继续执行下一步操作, 这并不代表 CPU 在这段时间内也会一直等待, 操作系统多半会切换到另一个线程上去, 等到调用返回后再切换回原来的线程。
异步就相反, 发起调用后, 线程继续向下执行, 当调用返回后, 通过某种手段来通知调用者。
同步和异步中的调用返回指的是内核进程将数据复制到调用进程, 顺序编程里面通常调用是同步的, 上一步执行之后才会继续下一步。
现在我写了一个爬虫爬取一个网站, 为了提高抓取效率, 我会使用多线程和多进程编程, 假设由于网络或者对方网站等问题, 某段时间的请求对方响应很慢, 即使设置了超时时间, 在超时时间到来之前, 这些线程都是处于等待的状态。这时候我可以实现一个功能, 使得程序遇到这种情况的时候先跳过这一步, 执行下一个请求, 等执行完之后, 再执行这个请求, 如果还是没有响应, 那么切换到下下个请求, 这样就可以通过切换任务尽量减少闲置时间, 异步编程就是实现这样的功能的。
单线程的同步模型
假设现在有三个包含独立任务的程序。在同一时刻, 只能有一个任务在执行, 并且前一个任务执行结束后其他的任务才能开始。如果任务都能按照事先规定好的顺序执行, 最后一个任务的完成意味着所有任务都完成。
多线程/多进程的同步模型
在多线程/多进程的同步模型里面, 每个任务都在单独的线程(进程)中完成。
这些线程都是由操作系统管理的, 如果在多核 CPU 的系统里面, 它们可能会相互独立运行, 若在单处理器环境下, 则会交错运行。在线程模式中, 具体哪个线程执行, 是由操作系统决定的, 作为开发者, 只需要简单地认为它们的指令流是相互独立且可以并行执行的。多进程模式同理, 可以分配任务给对应的进程。
多线程和多进程编程较为麻烦的是线程或者进程之间的通信以及数据管理的问题。
异步编程模型
单线程
。较多线程简单, 不需要担心操作系统收回任务的控制权, 因为决定权在开发者手中。任务交错执行
。开发者要做的就是将任务组织成一个序列来交替地小步完成, 每一个异步调用必须足够小, 不能耗时太久。
在上图不能明显体现出异步编程的优势, 而且任务的切换还会带来额外的开销。当任务强制等待或者阻塞的时候, 异步编程才能发挥优势, 如下图:
在上图中灰色部分代表这个时间段某个任务被阻塞, 阻塞的原因是等待 I/O 的完成(比如传输数据或者网络请求等等), 一个典型的 CPU 处理数据的能力是硬盘或者网络的几个数量级的倍数, 因此, 一个需要进行大的 I/O 操作的同步程序, 需要耗费大量的时间去等待硬盘或者网络将数据准备好, 正是因为这个原因, 同步的程序也叫阻塞程序。
阻塞/非阻塞
阻塞和非阻塞的概念是针对 IO 状态而言的, 关注的是程序在等待 IO 调用返回这段时间的状态。阻塞这个词来自操作系统的线程/进程的状态模型, 一个线程或者进程的执行会经历 创建
-> 就绪
-> 运行
-> 阻塞
-> 终止
5 个状态, 当线程需要某一个 IO 请求暂时得不到竞争资源时操作系统会把它阻塞起来避免浪费 CPU 资源, 等到有了资源再变成就绪的状态等待 CPU 的调度运行, 各个状态的转换如图所示:
阻塞和非阻塞, 以及同步和异步, 完全是两组概念, 它们之间并没有一个必然的联系。阻塞 != 同步, 非阻塞 != 异步。
I/O 编程模型
I/O 编程模型指操作系统在处理 IO 时所采用的方式, 这些模型是为了解决 I/O 速度问题而诞生的。UNIX 下的 I/O 编程模型主要有以下 5 种
blocking I/O
阻塞 I/O
。对于 I/O 来说, 通常分为两个阶段, 一个是准备数据, 一个是返回结果, 阻塞型 I/O 在进程发出一个系统调用请求之后, 进程就一直等待上述两个阶段完成, 等待拿到返回结果之后, 再重新运行。non-blocking I/O
非阻塞 I/O
。和阻塞 I/O 比较类似, 不同之处时当进程发起一个调用后, 如果数据还没有就绪, 就会马上返回一个结果, 告诉进程现在还没有就绪, 和阻塞 I/O 的区别就在于用户进程会不断地查询内核状态, 这个过程依然是同步的。I/O multiplexing
I/O 复用
。I/O 复用是以轮询的方式来查询内核的执行状态, 它和非阻塞 I/O 的区别是一个进程可能会管理多个 I/O 的请求, 当某个 I/O 调用有了结果之后, 就返回对应的结果。signal driven I/O
信号驱动式 I/O
。asynchronous I/O
异步 I/O
。当进程发出调用之后, 内核会立刻返回结果, 进程也会继续做其它事情, 直到操作系统返回数据之后, 会给用户进程发送一个信号。需要注意的是异步 I/O 并没有涉及任何关于回调函数的概念。
这 5 种模型中, 除了异步 I/O 之外, 其它的都是同步的。
异步模型具有优势的场景
有大量的任务, 因此在一个时刻至少有一个任务要运行。
任务执行大量的 I/O 操作, 这样同步模型就会在因为任务阻塞而浪费大量的时间。
任务之间相互独立, 以至于任务内部的交互很少。
这些条件大多是在 Client/Server 模式中的网络比较繁忙的服务器端出现, 比如 web 服务器, 每个任务代表一个客户端进行接收请求并回复的 I/O 操作, 客户端的请求相当于读操作, 它们是相互独立的, 因此, 网络服务是异步模型的典型代表。
事件驱动模型
事件驱动模型主要应用在图形用户界面(GUI)、网络服务和 Web 前端上。比如编写图形用户界面程序, 要给界面上每个按钮都添加监听函数, 而该函数只有在相应的按钮被用户点击的事件发生时才会执行, 开发者并不需要事先确定事件何时发生, 只需要编写事件的响应函数即可。监听函数或者响应函数就是所谓的事件处理器(event handler), 类似的事件还有鼠标移动、按下、松开、双击等等, 这就是事件驱动。
事件驱动的程序一般都有一个主循环(main loop)或称事件循环(event loop), 该循环不停地做两件事: 事件监测和事件处理。首先要监测是否发生了事件, 如果有事件发生则调用相应的事件处理程序, 处理完毕再继续监测新事件。事件循环只是在一个进程中运行的单个线程。