并发
Thread/Fiber/Ractor。
并发
这一课你将能做到
- 用 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)。