大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
并发
这一课你将能做到
- 用 Thread 创建和管理线程,理解 MRI 的 GIL 对并发的真实影响
- 用 Fiber 实现轻量级协程,说清 Fiber 与 Thread 的本质区别
- 用 Ractor 实现真正的并行计算,理解其无共享内存的设计哲学
为什么要学
并发是 Ruby 面试的「照妖镜」——很多人写了多年 Rails,被问到「Ruby 能不能真正并行」就露馅。GIL(全局解释器锁)让 MRI 的线程无法并行执行 Ruby 代码,但 IO 等待时可以释放;Ractor 是 Ruby 3.0 引入的真正并行方案。不懂这些,写并发代码就是踩雷。
人话:Ruby 的并发像「一家餐厅只有一个厨师(GIL)」。Thread 是多个服务员——一个服务员等菜时,另一个可以接待客人(IO 并发);但只有一个厨师在炒菜(CPU 不并行)。Ractor 是「多家分店」——每家有自己的厨师,真正并行,但分店之间不共享食材(无共享内存)。
先建立图像
Ruby 并发三剑客
┌──────────┬─────────────┬─────────────┬────────────────┐
│ 机制 │ 并行能力 │ 内存共享 │ 适用场景 │
├──────────┼─────────────┼─────────────┼────────────────┤
│ Thread │ IO 并发 │ 共享(危险) │ Web 请求、IO 密集 │
│ Fiber │ 协作式切换 │ 共享 │ 异步 IO、生成器 │
│ Ractor │ 真正并行 │ 不共享(安全) │ CPU 密集计算 │
└──────────┴─────────────┴─────────────┴────────────────┘
GIL 工作方式:
Thread A ──[Ruby 代码]──[IO 等待,释放 GIL]──▶ Thread B 获得 GIL
↑ 只有这里能切换
跟着做
步骤 1:Thread——IO 并发
# 基本线程创建
threads = 3.times.map do |i|
Thread.new do
sleep(rand(0.1..0.3)) # 模拟 IO 等待
puts "线程 #{i} 完成"
end
end
threads.each(&:join) # 等待所有线程结束
puts "全部完成"
# 线程间通信:Queue(线程安全)
queue = Queue.new
producer = Thread.new do
5.times do |i|
queue << "任务 #{i}"
sleep(0.05)
end
queue << nil # 结束信号
end
consumer = Thread.new do
while (item = queue.pop)
puts "消费: #{item}"
end
end
producer.join
consumer.join
# 共享变量的危险:竞态条件
counter = 0
threads = 10.times.map do
Thread.new { 1000.times { counter += 1 } }
end
threads.each(&:join)
puts "counter = #{counter}" # 大概率不是 10000!
# 解决:Mutex(互斥锁)
counter = 0
mutex = Mutex.new
threads = 10.times.map do
Thread.new { 1000.times { mutex.synchronize { counter += 1 } } }
end
threads.each(&:join)
puts "加锁后 counter = #{counter}" # => 10000
步骤 2:Fiber——协作式协程
# Fiber 是轻量级执行单元,主动让出控制权
fiber = Fiber.new do |first|
puts "Fiber 收到: #{first}"
second = Fiber.yield("第一次让出")
puts "Fiber 收到: #{second}"
"Fiber 结束"
end
puts fiber.resume("开始") # => Fiber 收到: 开始
# => 第一次让出
puts fiber.resume("继续") # => Fiber 收到: 继续
# => Fiber 结束
# Fiber 实现生成器
def fibonacci
Fiber.new do
a, b = 0, 1
loop do
Fiber.yield(a)
a, b = b, a + b
end
end
end
fib = fibonacci
10.times { puts fib.resume }
# => 0, 1, 1, 2, 3, 5, 8, 13, 21, 34
# Fiber 与 Thread 的关键区别:
# Thread 由操作系统调度,抢占式;
# Fiber 由程序员控制,协作式(resume/yield)。
步骤 3:Ractor——真正并行
# Ractor:Ruby 3.0+ 的真正并行方案
# 每个 Ractor 有独立的内存空间,不共享对象
# 基本创建与消息传递
ractor = Ractor.new do
received = Ractor.receive
"处理结果: #{received.upcase}"
end
ractor.send("hello")
puts ractor.take # => 处理结果: HELLO
# 多个 Ractor 并行计算
def heavy_compute(n)
(1..n).sum { |i| i * i }
end
# 串行
start = Time.now
2.times { heavy_compute(10_000_000) }
puts "串行: #{Time.now - start} 秒"
# Ractor 并行
start = Time.now
ractors = 2.times.map do
Ractor.new { heavy_compute(10_000_000) }
end
ractors.each(&:take)
puts "Ractor 并行: #{Time.now - start} 秒"
# Ractor 的限制:不能共享可变对象
# Ractor.new { shared_array << 1 } # 报错!IsolationError
步骤 4:concurrent-ruby 实战
# gem install concurrent-ruby
require "concurrent"
# ThreadPoolExecutor:线程池
pool = Concurrent::ThreadPoolExecutor.new(
min_threads: 2,
max_threads: 5,
max_queue: 10
)
futures = 10.times.map do |i|
Concurrent::Future.execute(executor: pool) do
sleep(0.1)
"任务 #{i} 完成"
end
end
futures.each { |f| puts f.value }
pool.shutdown
# Promise:链式异步
Concurrent::Promise.execute { 10 }
.then { |x| x * 2 }
.then { |x| puts "结果: #{x}" }
.rescue { |e| puts "错误: #{e}" }
.execute
讲清楚要点
- GIL 的真实含义:MRI 的 GIL 保证同一时刻只有一个线程执行 Ruby 字节码,但 IO 操作(网络、文件、数据库)会释放 GIL。所以 Ruby 线程对 IO 密集有效,对 CPU 密集无效。
- Thread 的共享内存是双刃剑:方便通信,但需要 Mutex/Queue 保护。Ruby 的
Queue是线程安全的,优先用它做生产者-消费者模式。 - Fiber 是「手动调度」的线程:Fiber 不会被抢占,只有
Fiber.yield时才切换。适合做生成器、异步 IO(如 Async gem),不适合并行计算。 - Ractor 是「无共享」的并行:Ractor 之间不能访问对方对象(除非是不可变对象或显式
shareable)。通信靠消息传递(send/receive/take),天然避免竞态条件。 - concurrent-ruby 是生产级选择:ThreadPoolExecutor、Future、Promise、Agent 等抽象比裸 Thread 更安全。Rails 的 ActiveJob、Sidekiq 底层都依赖它。
你可能会懵的地方
-
Ruby 到底能不能并行? MRI 的 Thread 不能并行执行 Ruby 代码(GIL),但 IO 可以并发;Ractor 可以真正并行,但限制多;JRuby/TruffleRuby 无 GIL,Thread 可以真并行。看实现,不看语言。
-
Fiber 和 Thread 哪个快? Fiber 创建和切换成本更低(用户态,无系统调用);但 Fiber 不能利用多核。IO 密集用 Fiber(如 Async gem),CPU 密集用 Ractor 或多进程。
-
Ractor 为什么不能共享对象? 共享可变对象是竞态条件的根源。Ractor 强制「无共享」,通过消息传递通信,从设计上消除了数据竞争。这是 Erlang/Actor 模型的思想。
-
Rails 怎么处理并发? 多进程(Puma 的 workers)+ 多线程(Puma 的 threads)+ 异步(ActionCable 用 Fiber)。数据库连接池、请求级线程隔离是 Rails 的并发保障。
-
Ruby 3.0 的 Fiber Scheduler 是什么? 允许自定义 Fiber 的调度器(如 Async gem 的
Async::Scheduler),让阻塞 IO 自动切换 Fiber。这是 Ruby 异步生态的基础设施。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 多个 Thread 直接改共享变量 | 竞态条件,结果随机 | 用 Mutex 或 Queue 保护 |
| 以为 Thread 能加速 CPU 计算 | GIL 限制,反而更慢 | CPU 密信用 Ractor 或多进程 |
| Fiber 里做阻塞 IO | 整个进程卡住 | 用 Fiber Scheduler + 非阻塞 IO |
| Ractor 里共享数组 | IsolationError | 用不可变对象或消息传递 |
| 线程不 join 就退出主线程 | 子线程被强制终止 | 主线程最后 threads.each(&:join) |
卡点急救
- 程序输出结果随机,时好时坏:竞态条件。检查共享变量是否加锁,或改用 Queue/Ractor。
- Ractor 报 IsolationError:试图共享可变对象。把对象改成
Ractor.make_shareable(obj)(冻结)或改用消息传递。 - Fiber 里 sleep 不切换:Fiber 是协作式,
sleep是阻塞调用,不会自动切换。用Fiber.scheduler或 Async gem。
面试常问
-
GIL 是什么?对 Ruby 并发有什么影响? GIL 是 MRI 的全局解释器锁,保证同一时刻只有一个线程执行 Ruby 字节码。影响:Thread 无法并行执行 Ruby 代码,但 IO 操作会释放 GIL,所以 IO 密集场景仍可用 Thread 提升吞吐量。
-
Thread、Fiber、Ractor 的区别和适用场景? Thread:OS 级,共享内存,适合 IO 并发;Fiber:用户态,协作式,适合异步 IO 和生成器;Ractor:真正并行,无共享内存,适合 CPU 密集计算。
-
Ruby 实现并发的最佳实践? IO 密集:Puma 多进程+多线程、Sidekiq 多线程;CPU 密集:Ractor 或 JRuby;异步 IO:Async gem(Fiber Scheduler)。避免裸 Thread 共享状态,优先用 concurrent-ruby 的抽象。
自测三问
- MRI 的 GIL 对 Thread 的 CPU 密集任务有什么影响?
- Fiber 的「协作式」和 Thread 的「抢占式」有什么区别?
- Ractor 为什么强制无共享内存?这解决了什么问题?
小结一句话
Ruby 并发的真相是:GIL 下 Thread 只能并发不能并行,Fiber 是协作式协程,Ractor 才是未来——但无共享的设计让写并行代码像写分布式系统。
延伸
- Ruby Ractor 官方文档
- concurrent-ruby 官方仓库
- 下一课:Gems 与框架——Bundler、Rack、Sinatra 与 Rails 生态。