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 很少是真实瓶颈。


参考文档

本文依据公开文档整理,示例需按项目依赖版本验证;不将文档中的性能描述视为本项目实测结果。

延伸阅读

获取专属方案

联系我们

加入社区

微信扫码
加入官方交流群

立即体验

在线开通,按量计费,真正的云服务!

立即开始

选择观测云版本

代码托管平台