Logo
Section 1 / 5

简介

本课程介绍了异步编程的概念,这是一种重要的编程范式。

导入

简介

本课程介绍了异步编程的概念,这是一种重要的编程范式。

简介

并发可以被定义为同时处理多个任务。即使在单核 CPU 的机器上,也可以并发运行多个进程或线程,但此时并非并行执行。并发使得我们能够在单核机器上创造出一种并行执行的假象,尽管实际上一次只能运行一个线程或进程。

并行是指同时执行多个任务。真正的并行只有在多核 CPU 的情况下才能实现。

一台拥有四个核心的机器,同时运行一百个进程,既是并行也是并发,但主要是并发,因为它一次最多只能处理四个任务。

在单核机器上实现并发的另一种范式是协作式多任务处理,接下来我们将讨论这一点。

协作式多任务处理

在单核机器上,通过上下文切换线程来获取 CPU 时间,从而实现多线程并发。当线程进行 I/O 操作或耗尽时间片时,操作系统会将其从 CPU 上移除。这种机制称为抢占式调度,即操作系统强制中断线程以释放 CPU 使用权。而 协作式多任务处理则采取不同的方式:正在运行的进程主动放弃 CPU 给其他进程使用。 例如,当进程因等待用户输入或发起网络请求而暂时无事可做时,它可以选择自愿让出 CPU。

进程调度器依赖于各个进程之间的相互配合来共享 CPU 资源,这也是这种多任务处理方式被称为“协作式多任务处理”或“非抢占式多任务处理”的原因。

Ruby 中的 Fiber 支持协作式多任务处理。任务的调度由程序员负责,如果某个任务行为异常,长时间占用 CPU 而不释放,就会导致其他任务无法执行,造成饥饿现象。

Ruby 的 Fiber 拥有自己的栈空间,可以被暂停和恢复。其概念上类似于协程。下面我们先澄清 Fiber 与相关概念的区别。

协程

协程是一种通用的编程概念,在多种编程语言中都有体现。协程可以被定义为一种特殊的函数,它可以在不丢失自身状态的情况下将控制权交还给调用者。 我们平时所熟悉的普通函数,它们在返回值后不再保留任何状态,可以看作是协程的一种特例,也称为子程序。

一些语言如 Python 还提供了类似的概念,称为生成器,本质上是迭代器,也是协程的一个子集。

Fiber

Fiber 本质上与协程是同一概念。例如,Python 中的协程在行为上与 Ruby 的 Fiber 类似。维基百科指出,两者唯一的区别在于:Fiber 是系统级构造,而协程是语言级构造。协程可以被视为 Fiber 的一种实现方式。从实际编程角度看,编写协程或 Fiber 并无本质差异。

此外,Fiber 的另一个优势是轻量级,相比线程更高效,且不会引发线程安全问题。使用 Fiber 时无需使用昂贵的锁机制或同步操作。

举个例子,我们可以用 Fiber 重写之前课程中的 ping-pong 示例。目前不必深究语法细节,注意以下代码中没有出现任何同步机制。

1pingFib = Fiber.new do 2 while true do 3 puts "ping" 4 Fiber.yield() 5 end 6end 7 8pongFib = Fiber.new do 9 while true do 10 puts "pong" 11 Fiber.yield() 12 end 13end 14 15 1610.times do 17 pingFib.resume() 18 pongFib.resume() 19end

异步编程

那么,为什么 Fiber 如此重要?因为它们支持协作式多任务处理,而协作式多任务处理为何重要?因为它使我们能够编写异步程序。让我们先通过一个类比来理解异步编程。

设想一家餐厅只有一位服务员。突然,三位顾客——Kohli、Amir 和 John 同时到来。他们各自决定点餐所需的时间不同:Kohli 需要 5 分钟,Amir 需要 10 分钟,John 只需 1 分钟。如果服务员先服务 Amir,耗时 10 分钟,接着服务 Kohli 用掉 5 分钟,最后服务 John 用掉 1 分钟,总共花费 16 分钟完成所有点单。然而在此过程中,John 等待了 15 分钟,Kohli 等待了 10 分钟,Amir 则无需等待。

现在假设服务员知道每位顾客需要多少时间做决定。他可以先服务 John(1 分钟),再服务 Amir(10 分钟),最后服务 Kohli(5 分钟)。这样每位顾客都无需等待,仿佛有三位专属服务员,但实际上只有一位。更重要的是,总耗时仅为 10 分钟,远低于之前的 16 分钟。

这个单一服务员的情景可以类比为单线程在处理多个任务。当某个任务因外部操作(如 I/O)阻塞时,线程立即转去处理其他已准备就绪的任务。这就是使用单线程实现异步处理的典型例子。

回到餐厅类比,如果我们增加服务员数量,就能服务更多顾客,这代表多线程下的异步处理。但在 MRI Ruby 中无法实现这一点,因为同一时刻只能有一个线程运行。

最后,为每个顾客分配一名专属服务员的做法,属于典型的多线程模式,但缺乏异步性,效率低下。若每个工作项都阻塞等待外部结果,而对应线程被完全占用,系统能处理的工作项数量将受到严重限制。

此外,当说一个线程在等待外部结果时,指的是底层硬件设备(如磁盘控制器、网卡等)正在处理请求,而操作系统线程本身处于空闲状态。在此期间,该线程完全占用内存和其他资源,却什么也不做。请记住,不存在隐藏的后台线程在为阻塞的线程生成结果。

我们可以通过单线程或多线程实现异步,但多线程并不等于异步。一个系统可以是多线程的,却完全没有异步特性。

引入异步机制可以显著提升系统的吞吐量——用相同的资源完成更多的工作。同样的服务员数量可以服务更多顾客。但异步并不会加快计算速度。 服务员无法比顾客做出选择更快地记录订单。

API 概述

事件循环与 Web 服务器