当多个线程同时访问同一个 Ruby IO::Buffer 实例时,如果不加任何协调,读写操作会相互穿插,轻则得到半新半旧的字节内容,重则触发底层内存访问错误。Ruby 为 IO::Buffer 提供了 lock_shared 和 lock_exclusive 两个实例方法,它们分别对标共享锁和排他锁,用来控制多线程环境下的读写顺序。

一、共享锁与排他锁的语义边界
lock_shared 获取的是共享锁,也就是通常所说的读锁。多个线程可以同时持有共享锁而互不阻塞,因为它们都承诺只读不写。共享锁的意义在于提升读多写少场景下的并发度,比如多个线程同时计算缓冲区的哈希值、遍历协议包或把数据拷贝到另一个结构。
排他锁则完全相反。lock_exclusive 获取写锁后,其他线程既不能获取共享锁,也不能获取排他锁,必须等待当前线程释放。排他锁保护的是会改变缓冲区状态的操作,包括写入字节、调整大小、释放底层内存或重新分配存储空间。只要这些操作被排他锁包裹,就不会出现一个线程正在写入而另一个线程同时读取同一段内存的情况。
下面这个例子用简单方式区分两类操作。读线程只查看缓冲区大小,写线程在获得排他锁后调整缓冲区容量。由于写锁会等待所有读锁释放,读操作不会被突然改变的内存布局干扰。
require "io/buffer"
buffer = IO::Buffer.new(1024)
readers = 3.times.map do
Thread.new do
buffer.lock_shared do
# 多个读取线程可以同时执行
buffer.size
end
end
end
writer = Thread.new do
buffer.lock_exclusive do
# 写线程在所有读锁释放前会阻塞
buffer.resize(2048)
end
end
readers.each { |t| t.join }
writer.join
二、lock_shared 与 lock_exclusive 的典型用法
只读任务最适合使用共享锁。例如一个网络服务收到完整数据包后,需要同时做协议解析、统计长度和生成日志摘要,这些任务可以交给不同线程。只要每个线程在访问前调用 lock_shared,它们就能并行执行,不会因为一个读取任务而阻塞其他读取任务。
写入任务则必须使用排他锁。假设一个缓冲区正在被多个消费者线程读取,主线程此时要更新协议头或重新分配更大的内存,如果不用排他锁,读线程可能拿到损坏的长度字段,甚至访问到已经释放的内存地址。使用 lock_exclusive 后,写入操作会等到所有共享锁释放,保证数据一致性。
在真实的网络协议或文件映射场景中,锁的边界最好尽量小。不要把耗时的计算放进排他锁块里,否则读线程会长时间饥饿;也不要为了省事在所有方法上加锁,这样会退化到单线程性能。通常做法是先在一个小排他锁里做状态切换,再在锁外执行与缓冲区无关的业务逻辑。
三、阻塞行为、重入与死锁风险
两个方法默认都会阻塞当前线程。当某个线程已经持有排他锁时,任何 lock_shared 调用都会等待;当缓冲区上存在任意数量的共享锁或一个排他锁时,lock_exclusive 也会等待。这种阻塞行为非常适合小块临界区,但一旦锁顺序设计不当,就会造成死锁。
最常见的死锁场景是锁升级。假设线程 A 先获取了共享锁,接着又尝试获取排他锁来修改数据。此时线程 A 会等待自己释放共享锁,但共享锁只有退出当前块才会释放,于是永远卡住。更复杂的情况是两个线程分别持有共享锁,然后都请求排他锁,这时谁也不会释放共享锁,形成循环等待。
为了避免这类问题,应当明确区分读写路径。读路径只获取共享锁,写路径只获取排他锁,不要在同一个线程的一个锁块内再获取更高级别的锁。如果确实需要读后写,可以先退出共享锁块,再进入排他锁块,最后根据最新状态决定是否执行写入。Ruby 还提供了非阻塞的 try_lock_shared 和 try_lock_exclusive,可以在获取失败时先处理其他任务,避免线程长时间悬挂。
四、与 Mutex 和文件锁的对比
Ruby 自带的 Mutex 是最常用的互斥锁,它只允许一个线程进入临界区,即使是纯读取操作也会被串行化。如果缓冲区读多写少,用 Mutex 会损失明显的并发吞吐。IO::Buffer 的共享锁则允许多个读取线程并行,只有在写入时才互斥,更适合读多写少的字节处理。不过 Mutex 概念更简单,写多读多时也可能比复杂的读写锁更容易维护。
还需要区分进程内锁和跨进程锁。IO::Buffer 的 lock_shared 与 lock_exclusive 只在当前 Ruby 进程内协调线程,无法阻止另一个进程修改同一块内存。如果需要跨进程同步,应该使用文件锁、管道或数据库锁等机制。IO::Buffer 的锁解决的是线程安全问题,不是跨进程互斥问题。
选型时可以这样判断:如果只有多个线程读取和写入同一个 IO::Buffer,且读取次数明显多于写入次数,优先使用共享锁和排他锁的组合;如果写入和读取几乎一样频繁,Mutex 可能更简单,开销差异也不大;如果涉及多进程或分布式系统,则需要引入外部协调机制。明确这些边界之后,再决定使用哪种锁,可以少走很多弯路。
Ruby IO::Bufferlock_sharedlock_exclusive修改时间:2026-09-20 06:46:40