- 原文作者:Bruno Sutic
- 原文链接: 《Ruby Fiber Scheduler》
- 原文时间:2022年2月25日
- 原文讨论:Hacker News 讨论
- 译者:Mark24
- 译者 Email:mark.zhangyoung@gmail.com
- 译文链接:mark24code.github.io/ruby/2023/1…
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_scheduler
和 Fiber.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!