Ruby 线程与并发完全指南:GIL 的真相与实践
Ruby 的 GIL 让线程无法并行执行 CPU 任务,但 I/O 并发依然强大。本文讲清 GIL 原理、Thread 用法、线程间通信、同步机制(Mutex/Queue),以及何时该用进程、Fiber 或 Ractor。
Ruby 线程受 GIL(全局解释器锁)限制,同一时刻只有一个线程执行 Ruby 代码——CPU 密集型任务用多线程没有收益(要用多进程或 Ractor);但线程等待 I/O(网络、数据库、文件)时会释放 GIL,所以 I/O 密集型任务的多线程并发效果显著,Puma、Sidekiq 的高并发正建立于此。
GIL:并发与并行的分水岭
CRuby(MRI)解释器里有一把全局锁:任何时刻只允许一个线程执行 Ruby 字节码。这保证了解释器内部数据结构的安全,但也意味着八个线程跑纯计算,总吞吐约等于一个线程——这不是 bug,是 MRI 的设计取舍(JRuby、TruffleRuby 没有 GIL,是真正的并行)。
关键在"释放点":线程发起 I/O 调用(等数据库返回、等 HTTP 响应、读文件)时释放 GIL,其他线程趁机运行。I/O 密集的 Web 应用会花较多时间等待外部操作——这就是 Ruby 多线程服务器依然高效的原因。
Thread 基本用法
threads = 5.times.map do |i|
Thread.new do
result = fetch_data(i) # I/O 操作:释放 GIL,与其他线程并行等待
puts "线程 #{i} 完成:#{result}"
end
end
threads.each(&:join) # 等待全部完成
对比串行执行:5 个各耗时 1 秒的 HTTP 请求,串行 5 秒,多线程约 1 秒——I/O 并发的收益立竿见影。
返回值与线程间通信
t = Thread.new { expensive_calculation }
result = t.value # 阻塞取结果(异常也会在此抛出)
# Queue:线程安全通信的标准方式
queue = Queue.new
producer = Thread.new { 10.times { |i| queue << i }; queue.close }
consumer = Thread.new do
while (item = queue.pop)
process(item)
end
end
[producer, consumer].each(&:join)
Queue(以及 SizedQueue 背压控制)是生产者-消费者模式的正确答案——线程间永远用 Queue 传数据,不要共享裸变量。
线程安全与同步
共享可变状态是多线程的头号杀手:
# 错误:counter += 1 不是原子操作(读-改-写三步),并发下丢更新
# 正确:用互斥锁
mutex = Mutex.new
counter = 0
threads = 10.times.map do
Thread.new do
1000.times { mutex.synchronize { counter += 1 } }
end
end
threads.each(&:join)
counter # => 10000(无锁时结果随机小于此值)
纪律三条:默认所有共享对象都需要保护;能用不可变数据就不用锁;优先用并发原语(Concurrent::Hash、Concurrent::AtomicFixnum,来自 concurrent-ruby gem)而不是手动 Mutex。
超出线程的选择
- CPU 密集:多进程(
Process.fork、Puma cluster 模式)绕开 GIL;或 Ruby 3.x 的 Ractor(真并行,但生态尚早期); - 轻量并发:Fiber + Async gem(协程式并发,单线程内调度数万连接)——Falcon 服务器路线;
- Web 服务器:Puma 的多进程 × 多线程混合模型是生产标准,进程数吃满 CPU 核、线程数吃满 I/O 等待。
常见问题(FAQ)
Q:Rails 应用是线程安全的吗?
Rails 自身是(每个请求独立处理),但你的代码未必:类变量 @@x、全局变量、请求间共享的单例可变状态都是地雷。Puma 多线程模式下任何"存起来下次用"的对象都要过一遍线程安全检查。
Q:线程数设多少合适?
I/O 密集场景经验值 5-16(Puma 默认 5),具体看 I/O 等待占比和数据库连接池大小——线程数超过连接池会互相饿死。压测调优,不要拍脑袋。
Q:GIL 会被移除吗?
短期内不会(兼容性代价巨大),但 Ruby 在持续进化:Ractor 提供真并行、MJIT/YJIT 提升单线程性能、Fiber 调度器完善协程生态。实践中"多进程 + 多线程"的 Puma 模型已能榨干硬件,GIL 很少是真实瓶颈。
参考文档
本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。