【翻译】Ruby Fiber Scheduler

Fiber Scheduler(纤程调度器)在 Ruby 中实现异步编程。该功能是 Ruby 3.0 的一大增强功能,并且也是优秀的 async gem 的核心组件之一。 最棒的一点是,你并不需要一个完整的框架就能开始!只需使用一对内置的 Ruby 方法,就能独立地实现纤程调度器并享受到异步编程的好处。

纤程调度器主要包括两部分:

  • Fiber Scheduler interface(纤程调度器接口) 这是一套内置于编程语言中的阻塞操作钩子。钩子实现被委托给 Fiber.scheduler 对象。

  • Fiber Scheduler implementation(纤程调度器的实现) 实现了异步行为。这是一个需要程序员显式设置的对象,因为 Ruby不提供默认的 Fiber Scheduler(纤程调度器)实现。

非常感谢 Samuel Williams!他是 Ruby 的核心开发者,设计并实现了纤程调度器这一功能并整合到了语言中。

Fiber Scheduler interface(纤程调度器接口)

Fiber Scheduler(纤程调度器)接口是一套阻塞操作的钩子,它允许在阻塞操作发生时插入异步行为。它像是带有反转的回调:当异步回调被执行时,主阻塞方法不会运行。 这些钩子在 Fiber::SchedulerInterface 类中有文档记录。这个 Ruby 功能背后的一些主要思想包括:

  • 钩子是低层级的。这导致了少量的钩子,每个钩子处理许多高层级方法的行为。例如,#address_resolve 钩子负责处理大约 20 个方法。
  • 钩子只在 Fiber.scheduler 对象设置后才会工作,钩子的实现被委托给该对象。
  • 钩子的行为应该是异步的。

Hook implementation (钩子实现)

让我们看一个示例,显示如何实现 Kernel#sleep 钩子。在实践中,所有的钩子都是用 C 语言编写的,但为了清晰起见,这里使用了 Ruby 伪代码。

module Kernel
  def sleep(duration = nil)
    if Fiber.scheduler
      Fiber.scheduler.kernel_sleep(duration)
    else
      synchronous_sleep(duration)
    end
  end
end

以上代码的阅读方式如下:

  • 如果设置了 Fiber.scheduler 对象 - 运行其 #kernel_sleep 方法。#kernel_sleep 应该异步运行 sleep
  • 否则,执行常规的 synchronous_sleep,它会阻塞当前线程直到 sleep 完成。

其他的钩子的工作方式也类似。

Blocking operations(阻塞操作)

已经多次提到了”Blocking operations(阻塞操作)”这个概念,但它到底是什么意思呢?阻塞操作是指任何Ruby进程(更具体地说:当前线程)最终会等待的操作。一个更具描述性的名称是“waiting operations(等待操作)”。 一些例子如下:

  • sleep 方法。
  • I/O操作如 URI.open("https://brunosutic.com")
  • 系统命令,例如 curl https://www.ruby-lang.org
  • 通过 Thread#join 等待线程结束。

作为一个反例,以下代码片段需要一段时间才能完成,但不包含阻塞操作:

def fibonacci(n)
  return n if [0, 1].include? n

  fibonacci(n - 1) + fibonacci(n - 2)
end

fibonacci(100)

获取 fibonacci(100) 的结果需要等待很长时间,但只有程序员在等待!整个时间 Ruby 解释器都在工作,后台进行计算。一个简单的斐波那契实现并不包含阻塞操作。

发展对阻塞操作是什么(和不是什么)的直觉是值得的,因为异步编程的整个目标就是同时等待多个阻塞操作

Fiber Scheduler implementation(纤程调度器实现)

纤程调度器实现是 Fiber Scheduler 功能的第二大部分。

如果你想在 Ruby 中启用异步行为,你需要为当前线程设置一个 Fiber Scheduler 对象。这是通过 Fiber.set_scheduler(scheduler) 方法完成的。实现通常是一个定义了所有 Fiber::SchedulerInterface 方法的类。

Ruby 不提供默认的 Fiber Scheduler 类,也没有可以用于此目的的对象。这看起来不寻常,但实际上不将 Fiber Scheduler 实现包含在语言中是一个好的长期决定。最好将这种相对快速演变的关注点留在 Ruby 核心之外。 从头开始编写 Fiber Scheduler 类是一项复杂的任务,所以最好使用现有的解决方案。实现的列表,它们的主要区别和推荐可以在 Fiber Scheduler List 项目中找到。

举个例子

让我们来看看仅使用 Fiber Scheduler 可以做什么。 所有示例都使用 Ruby 3.1 和来自 fiber_scheduler gem 的 FiberScheduler 类,这个 gem 由我维护。这个 gem 对于示例来说不是一个硬性依赖项,因为如果将以下代码片段中的 FiberScheduler 替换为另一个 Fiber Scheduler 类,每个代码片段仍然应该可以工作。

基本示例

这里有一个简单的示例:

require "fiber_scheduler"
require "open-uri"

Fiber.set_scheduler(FiberScheduler.new)

Fiber.schedule do
  URI.open("https://httpbin.org/delay/2")
end

Fiber.schedule do
  URI.open("https://httpbin.org/delay/2")
end

上面的代码创建了两个纤程,每个纤程都进行一次 HTTP 请求。这些请求并行运行,整个程序在 2 秒内完成。

  • Fiber.set_scheduler(FiberScheduler.new) 在当前线程中设置一个 Fiber Scheduler,这使得 Fiber.schedule 方法可以工作,且 fiber 可以异步行为。

  • Fiber.schedule { ... } 这是一个内置的 Ruby 方法,用于启动新的异步 fiber。

这个示例仅使用了标准的 Ruby 方法 - Fiber.set_schedulerFiber.schedule 自 Ruby 3.0 版本以来就一直可用。

高级例子

我们来看看运行多种不同操作是什么样子的:

require "fiber_scheduler"
require "httparty"
require "open-uri"
require "redis"
require "sequel"

DB = Sequel.postgres
Sequel.extension(:fiber_concurrency)

Fiber.set_scheduler(FiberScheduler.new)

Fiber.schedule do
  URI.open("https://httpbin.org/delay/2")
end

Fiber.schedule do
  # Use any HTTP library
  HTTParty.get("https://httpbin.org/delay/2")
end

Fiber.schedule do
  # Works with any TCP protocol library
  Redis.new.blpop("abc123", 2)
end

Fiber.schedule do
  # Make database queries
  DB.run("SELECT pg_sleep(2)")
end

Fiber.schedule do
  sleep 2
end

Fiber.schedule do
  # Run system commands
  `sleep 2`
end

如果我们顺序运行这个程序,它大约需要12秒才能完成。但是由于这些操作是并行运行的,所以总的运行时间仅仅超过2秒。 你并不仅限于发起 HTTP 请求。任何内置在 Ruby 中或由外部 gem 实现的阻塞操作都可以工作!

扩展示例

这是一个简单的,显然是人为刻意的示例,同时运行一万个操作。

require "fiber_scheduler"

Fiber.set_scheduler(FiberScheduler.new)

10_000.times do
  Fiber.schedule do
    sleep 2
  end
end

上述代码的完成时间略超过2秒。

由于其低开销,sleep 方法被选择用于扩展示例。如果我们使用网络请求,由于需要建立数千个连接并进行 SSL 握手等,执行时间将会更长。

异步编程的主要优势之一是能够同时等待许多阻塞操作。阻塞操作数量的增加将增加这种优势。幸运的是,运行大量协程(fibers)非常简单。

结论

Ruby只需要一个纤程调度器(Fiber Scheduler)和一些内置方法就可以异步工作 - 不需要任何框架!

使其工作很容易。选择一个纤程调度器(Fiber Scheduler)实现,然后使用以下这些方法:

  • Fiber.set_scheduler(scheduler)为当前线程设置一个纤程调度器(Fiber Scheduler),使阻塞操作能够异步执行。
  • Fiber.schedule { ... } 启动一个新的纤程,该纤程与其他纤程并发运行。

一旦你开始运行,你可以通过将它包装在一个 Fiber.schedule 块中来使任何代码异步化

Fiber.schedule do
  SynchronousCode.run
end

整个库可以轻松地使用这种方法转换为异步,而且往往不需要比这里展示的更多努力。

异步编程的重大好处是并行化阻塞/等待操作以减少程序运行时间。这通常意味着在单个CPU上运行更多的操作,或者更好地,在你的Web服务器上处理更多的请求。

祝你使用纤程调度器(Fiber Scheduler)愉快!

Happy hacking with Fiber Scheduler!

Mark24

Everything can Mix.