根据Ruby(MRI)中的GIL实现,下面的代码必须通过多次打印消息而失败。但它没有,它总是打印一次:
class Sheep
def initialize
@shorn = false
end
def shorn?
@shorn
end
def shorn!
puts "shearing..."
@shorn = true
end
end
s = Sheep.new
55.times.map do
Thread.new { s.shorn! unless s.shorn? }
end.each(&:join)
怎么回事?
$ ruby --version
ruby 2.1.2p95 (2014-05-08 revision 45877) [x86_64-darwin13.0]
答案 0 :(得分:2)
这取决于您使用的确切ruby版本(它们调度线程的方式不同)。在我的系统上,它取决于整体系统负载和终端感觉有多快,但在Ruby 2.0.00p481上我得到1到55行输出,在Ruby 1.8.7上,我一直只得到一行。
应该注意的是,Ruby 2.0及更高版本使用实际的OS线程(尽管仍然使用GIL),而Ruby 1.8使用内部绿色线程和自己的调度。旧的ruby版本很可能会更精细地安排线程。
在任何情况下,您都不应该依赖任何偶然的线程调度行为。这不是任何记录行为的一部分,并且将在Ruby成熟时在不同系统上发生变化。在使用线程时,应始终确保安全地使用共享数据结构。
答案 1 :(得分:0)
我使用Ruby版本ruby 2.1.5p273
,我认为你的略有不同的Ruby版本会产生类似的结果。
每次运行程序时都会有不同的结果。
我尝试启用了一个核心并启用了前核心。我没有看到差异。正如您所料,它不是线程安全的。
否则,我能提出的唯一答案是你的程序太快/轻量级,因此解释器不会过于频繁地考虑线程切换。
在这种情况下我只有一个建议。你可以使用一个技巧给解释器一个提示,也许她可以切换线程。您可以使用sleep
功能。
在你的例子中,我会把它放在race condition
:
def shorn!
sleep 0.0001
puts "shearing..."
@shorn = true
end
如果您想了解有关GIL的更多信息,我可以推荐Jesse Storimer Nobody understands the GIL
如果您想了解有关Ruby和并发的更多信息,我可以推荐Dotan Nahum' Pragmatic Concurrency with Ruby
我建议的技巧是in this answer
答案 2 :(得分:0)
正如其他人所提到的,GIL的行为没有记录,完全依赖于实现。你不应该依赖于对其调度行为的任何期望。
然而,更详细(也更一般)的答案是调度程序在线程之间切换执行以确保没有单个线程阻塞进程。此开关称为上下文切换,或者更具体地称为线程切换。
当发生上下文切换时,暂停当前线程的执行并恢复另一个线程的执行。如果它是一个正在恢复的全新线程,"那么这意味着新线程的执行从头开始。
对于您的程序,每个新线程都以
开头s.shorn?
评估unless s.shorn?
。此时,@shorn == false
和s.shorn?
的计算结果为false。那么线程运行:
s.shorn!
#shorn!
中运行的第一个命令是:
puts "shearing..."
接下来会发生什么取决于线程调度程序:
@shorn = true
。然后线程结束,调度程序启动下一个线程,unless s.shorn?
计算结果为true,线程停止。此行为在循环中重复,直到没有剩余的线程为止。@shorn = true
之前暂停执行,并从头开始运行与之前相同的代码。 这意味着新线程启动时@shorn == false
,因此puts "shearing..."
将再次执行。如您所见,这完全取决于调度程序何时决定执行上下文切换。
GIL是MRI Ruby的可怕误解部分。有很多资源可以解释GIL是如何工作的,但在这种情况下,你应该知道的最重要的事情是GIL并不能保证每个线程都能顺序运行。
相反,GIL只是保证在C中实现的大多数核心Ruby方法(例如,Array#<<
)不会被上下文切换中断,直到它们完成为止。在puts "shearing..."
的情况下,我没有查看puts
的代码,但可能GIL保证在当前运行的线程完成执行puts
之前不会运行任何其他线程
至于为什么在MRI 1.8.7下运行代码时,它只显示shearing...
一次,这与绿色线程和本机线程无关。更好的答案是这是巧合。更精确的答案是,在您的情况下,由于某种原因,调度程序决定在运行@shorn = true
后中断第一个线程。这种行为可能是由于绿色线程在某种意义上可能是您的本机调度程序比Ruby的调度程序更频繁地中断(因此在下面的一个答案中提出了更细粒度的建议),但是这不一定是真的。它也可能是一个侥幸。
Ruby中的多线程很容易搞砸。因此,为什么Matz建议坚持使用分叉进程,这种进程效率低,但却减轻了管理线程的负担。大型项目的另一种方法是使用像Celluloid这样的库,它抽象出Ruby的线程安全机制。但是,对于像这样的小例子,一个简单的互斥量就可以了:
semaphore = Mutex.new
s = Sheep.new
55.times.map {
Thread.new {
semaphore.synchronize do
s.shorn! unless s.shorn?
end
}
}.each(&:join)