Ruby闭包、线程与作用域问题排查
嘿,这个问题其实是Ruby闭包和线程执行时机的经典“坑”,我来帮你理清楚前因后果,再给你靠谱的解决办法!
你说得没错,Thread.new的块确实是闭包,但它捕获的是start和finish这两个变量的引用,而不是调用Thread.new那一刻的变量值。
主线程的循环跑得很快——创建完一个线程后,立刻就更新了start的值。如果新线程还没来得及启动执行(比如主线程调度优先级更高),等它真正跑到threadRoutine(start,finish)这行时,拿到的就是已经被主线程修改后的变量值了。前两个线程看起来正常,只是因为它们刚好在主线程更新变量前就完成了启动和参数读取,纯属运气~
完全不用靠sleep这种不靠谱的临时方案,有两种稳定的解决思路:
方法1:直接给Thread.new传递参数
Ruby的Thread.new本身支持直接传参数给块,这时块会捕获参数的当前值副本,而不是外部变量的引用。修改你的代码如下:
def threadRoutine(p1,p2) puts "Thread #{Thread.current.object_id} started with parameters #{p1}, #{p2}" Kernel.sleep 5 puts "Thread #{Thread.current.object_id} completed" end start = 0 threads = [] (1..10).each do finish = start + 1 # 把start和finish作为参数传给Thread.new的块 threads << Thread.new(start, finish) { |p1, p2| threadRoutine(p1, p2) } puts "Started thread #{threads[-1].object_id} with parameters #{start},#{finish}" start = finish end threads.each do |t| t.join end
这样每个线程创建时,start和finish的当前值会被复制为线程块的参数,不管主线程后续怎么修改这两个变量,线程里拿到的都是创建那一刻的固定值。
方法2:用局部变量捕获当前值
如果不想改变Thread.new的传参方式,也可以在循环块内部创建局部变量,把当前的start和finish存起来。闭包会捕获这个局部变量,而它不会被主线程后续的修改影响:
def threadRoutine(p1,p2) puts "Thread #{Thread.current.object_id} started with parameters #{p1}, #{p2}" Kernel.sleep 5 puts "Thread #{Thread.current.object_id} completed" end start = 0 threads = [] (1..10).each do finish = start + 1 # 用局部变量保存当前循环的变量值 current_start = start current_finish = finish threads << Thread.new{threadRoutine(current_start, current_finish)} puts "Started thread #{threads[-1].object_id} with parameters #{start},#{finish}" start = finish end threads.each do |t| t.join end
每次循环迭代都会创建新的current_start和current_finish,它们是独立的局部变量,主线程后续修改start完全不会影响到已经捕获这些变量的线程闭包。
sleep本质是让主线程暂停,给新线程足够的时间启动并执行到threadRoutine的第一行——这时已经读取了当时的变量值。但这种方法完全依赖系统调度的时间,非常不可靠:如果系统负载高、线程启动慢,问题还是会复现,绝对不能用在生产代码里。
内容的提问来源于stack exchange,提问作者Matthew K

