引子
Java 系中有明确的可重入锁的概念, Ruby 的锁靠 Mutex 实现, 是不可重入锁, 在同一个线程中, Mutex 嵌套会报死锁异常.
@mutex = Mutex.new
@mutex.synchronize do
@mutex.synchronize do
puts '>'
end
end
=begin
deadlock; recursive locking (ThreadError)
=end
需求
class Worker
def initialize
@mutex = Mutex.new
end
def call
@mutex.synchronize do
yield
end
end
def process
call do
@mutex.synchronize do
p "process>>>"
end
end
end
end
Worker.new.process
=begin
in `synchronize': deadlock; recursive locking (ThreadError)
=end
如上例所示, 我们要求加锁执行 call
方法, 并且该方法要执行传入的 block.
一旦 block 也持有同样的锁, 那么就会出现以上的 Mutex 重入问题.
解决
MonitorMixin 提供了一种可重入锁的实现, 不需要手动定义锁, 可以多次进入锁.
require 'monitor'
class Worker
include MonitorMixin
def initialize(*args)
super
end
def call
synchronize do
yield
end
end
def process
call do
synchronize do
p "process>>>"
end
end
end
end
Worker.new.process
MonitorMixin
模块可以 extend
到一个 object 上, 或者在类中 include
(需要重写 initialize
方法), 也可以单独使用 Monitor
类,
具体参见 https://devdocs.io/ruby~2.6/monitormixin.
补充
Mutex 的不可重入特性仅限制在同一个线程内, 锁内开启新线程后, 在新线程内使用同一个 Mutex 是合理的.
@mutex = Mutex.new
@resource = ConditionVariable.new
@mutex.synchronize do
p '0'
thr = Thread.new do
@mutex.synchronize do
p '2'
@resource.signal
sleep 0.1
p '3'
end
end
p '1'
@resource.wait(@mutex)
p '4'
end
=begin
"0"
"1"
"2"
"3"
"4"
=end
上例中, 主线程派生出 thr 后, 立即执行 p '1'
, 因为等待 @resource
而释放 @mutex
锁, 同时交出执行权.
执行分片来到 thr 上, 顺序执行, @resource.signal
唤醒了主线程 (但此时主线程还没有获得锁, 依旧不能执行).
等 thr 睡一会再打印完 p '3'
后, thr 释放锁, 此时已经被唤醒的主线程得到锁继续执行, 打印 p '4'
.
理解了这里就能理解 Puma 新版本的 ThreadPool 是如何派生初始数量的工作线程了.
参考
https://devdocs.io/ruby~2.6/monitormixin