处理 I/O 的策略

在我们深入编写一些代码之前,我们将讨论处理 I/O 和并发的不同策略来结束本书的这一部分。 请注意,我会在这里概括地介绍 I/O,但使用网络通信 (network communication) 作为主要示例。 不同的策略可以有不同的优势,这取决于 I/O 类型。

1. 使用 OS 线程

第一种方法是让 OS 为我们处理一切。 我们通过给我们想要完成的每个任务生成 (spawn) 一个新的 OS 线程来做到这一点, 并像往常一样编写代码。

优点:

  • 简单
  • 易于编写
  • 性能还不错
  • 免费(不费力气地)获得并行性

缺点:

  • OS 线程具有相当大的堆栈。 如果你有许多任务同时等待(就像在重负载情况下的网络服务器中一样),内存很快就会耗尽。
  • 涉及很多系统调用。当任务数量很高时,这可能会非常昂贵。
  • OS 有很多事情需要处理。它可能不会像你希望的那样快速切换回你的线程
  • OS 不知道优先处理哪些任务,你可能希望为某些任务赋予比其他任务更高的优先级。

2. 绿色线程

另一种常见的处理方式是绿色线程。像 Go 这样的语言使用它取得了巨大成功。 绿色线程在许多方面与 OS 线程的功能相似,但绿色线程可以更好地调整运行时并适合你的特定需求。

优点:

  • 使用简单。代码看起来像使用 OS 线程一样。
  • 性能还不错。
  • 大量内存使用不是问题。
  • 你可以完全控制线程的调度方式,如果需要,你可以给不同线程的不同的优先级。

缺点:

  • 你需要一个运行时,并且通过它来重复 OS 已经完成的一些工作。 在某些情况下,运行时将产生可能很大的成本。
  • 可能难以以灵活的方式实施以处理各种任务。

3. OS 支持的基于轮询的事件循环

第三种方式是最接近理想解决方案的方式。 在这个解决方案中,我们注册 (register,可以理解为记录) 一个感兴趣的事件, 然后让 OS 告诉我们数据什么时候准备好了。

它的工作方式是:我们告诉 OS 我们对数据何时到达网卡上感兴趣。 当发生某些事情时,网卡会发出中断,此时,驱动程序 (driver) 会让 OS 知道数据已准备就绪。

现在,我们仍然需要一种在等待过程中“暂停”任务的方法, 这就是 Node 的“运行时” (runtime) 或 Rust 的 Futures 发挥作用的地方。

优点:

  • 接近最佳资源利用率
  • 它非常高效
  • 给我们最大的灵活性来决定如何处理发生的事件

缺点:

  • 不同的操作系统有不同的方式来处理这些队列。其中一些很难相互协调。某些操作系统对支持此方法的 I/O 操作有限制。
  • 极大的灵活性伴随着大量的复杂性。
  • 难以编写抽象层的、符合人体工程学的 API, 因为当前 API 在不引入不必要的成本的情况下负责处理 OS 之间的差异。
  • 只解决了部分问题 —— 程序员仍然需要一种策略来暂停正在等待的任务。

最后说明

Node runtime 组合了第 1 和第 3 种方法,但它试图强制所有 I/O 使用方法 3 。 这种设计也是 Node 非常擅长并发处理许多连接的部分原因。 Node 使用基于回调的方法来暂停任务。

Rust 的异步是围绕方法 3 建模的,它花费很长时间来稳定异步, 其中一个原因与以下两点有关:方法 3 的缺点、选择一种方法建立“任务应该如何暂停”的模型。 Rust 的 futures 将任务建模为 状态机 (State Machine), 其中暂停点 (suspension point) 代表“状态”。