深入探索
传递执行权给其它线程
在某些情况下,你可能特别希望某个线程(thread)能够让步执行权(execution)给任何其它线程以让其运行。例如,如果你有多个线程正在进行稳定的更新图形操作或显示各种“正在发生的”统计信息,你可能需要确保一旦一个线程绘制了 X 个像素或显示了 Y 个统计数据,另一个线程保证有机会做一些其它事情。
从理论上讲,Thread.pass
方法可以解决这个问题。根据 Ruby 的源代码文档,Thread.pass
调用线程调度程序将执行权传递给另一个线程。这是 Ruby 文档提供的示例:
a = Thread.new { print "a"; Thread.pass;
print "b"; Thread.pass;
print "c" }
b = Thread.new { print "x"; Thread.pass;
print "y"; Thread.pass;
print "z" }
a.join
b.join
根据文档,此代码在运行时会生成以下输出:
axbycz
是的,确实如此。理论上,这似乎表明,通过在每次调用 print
之后调用 Thread.pass
,这些线程将执行权传递给另一个线程,这就是两个线程的输出交替的原因。
出于我心中的疑问,我想知道 Thread.pass
的调用被删除后会产生什么影响?第一个线程是否会一直占用,只有在结束后才让步于第二个线程?找出答案的最佳方法是尝试:
a = Thread.new { print "a";
print "b";
print "c" }
b = Thread.new { print "x";
print "y";
print "z" }
a.join
b.join
如果我的理论是正确的(该线程将一直占用,直到它完成),这将是预期的输出:
abcdef
事实上,(令我惊讶的是!),实际产生的输出是:
axbycz
换句话说,无论是否调用 Thread.pass
,结果都是相同的。那么,Thread.pass
做什么呢?其宣称 pass
方法,调用线程调度程序将执行权传递给另一个线程,该文档是错误的吗?
在一个短暂而愤怒的时刻,我承认我轻率的认为有一种可能性,文档是不正确的,并且 Thread.pass
根本没有做任何事情。深入研究 Ruby 的 C 语言源代码很快消除了我的疑虑;Thread.pass
确实做了一些事情,但它的行为并不像 Ruby 文档暗示的那样可预测。在解释原因之前,让我们尝试一下我自己的示例:
s = 'start '
a = Thread.new { (1..10).each{
s << 'a'
Thread.pass
}
}
b = Thread.new { (1..10).each{
s << 'b'
Thread.pass
}
}
a.join
b.join
puts( "#{s} end" )
乍一看,这可能看起来与前面的示例非常相似。它设置两个线程运行,但不是反复打印东西出来,而是重复地将一个字符附加到字符串中 - ‘a’ 由线程 a
添加,’b’ 由线程 b
添加。每次操作后,Thread.pass
将执行权传递给另一个线程。最后显示整个字符串。字符串包含 ‘a’ 和 ‘b’ 的交替序列应该不足为奇:
abababababababababab
现在,请记住,在上一个程序中,即使我删除了对 Thread.pass
的调用,我也获得了完全相同的交替输出。基于这种经历,如果我在这个程序中删除 Thread.pass
,我想我应该期望得到类似的结果。我们来试试吧:
s = 'start '
a = Thread.new { (1..10).each{
s << 'a'
}
}
b = Thread.new { (1..10).each{
s << 'b'
}
}
a.join
b.join
puts( "#{s} end" )
这次,输出如下:
aaaaaaaaaabbbbbbbbbb
换句话说,这个程序显示了我最初在第一个程序中预料的那种不同的行为(我从 Ruby 的嵌入式文档中复制出来的那个)- 也就是说当两个线程在它们自己的时间片下运行时,第一个线程,a
,抢占所有时间为它自己所用,只有当它完成时第二个线程 b
才会得到关注。但是通过显式添加对 Thread.pass
的调用,我们可以强制每个线程将执行权传递给任何其它线程。
那么我们如何解释这种行为上的差异呢?从本质上讲,pass0.rb 和 pass3.rb 正在做同样的事情 - 运行两个线程并显示每个线程的字符串。唯一真正的区别在于,在 pass3.rb 中,字符串在线程内连接而不是打印。这可能看起来不是什么大不了的事,但事实证明,打印字符串比连接字符串需要更多的时间。实际上,print
调用会引入时间延迟。正如我们之前发现的那样(当我们有意使用 sleep
引入延迟时),时间延迟对线程产生了深远的影响。
如果你仍然不相信,请尝试我重写的 pass0.rb 版本,我创造性地命名为 pass0_new.rb。这只是用连接替换了打印。现在,如果你对 Thread.pass
的调用进行注释和取消注释,你确实会看到不同的结果。
s = ""
a = Thread.new { s << "a"; Thread.pass;
s << "b"; Thread.pass;
s << "c" }
b = Thread.new { s << "x"; Thread.pass;
s << "y"; Thread.pass;
s << "z" }
a.join
b.join
puts( s )
顺便说一句,我的测试是在运行 Windows 的 PC 上进行的。很可能在其它操作系统上会看到不同的结果。这是因为控制分配给线程的时间量的 Ruby 调度程序的实现在 Windows 和其它操作系统上是不同的。在 Unix 上,调度程序每 10 毫秒运行一次,但在 Windows 上,通过在某些操作发生时递减计数器来控制时间共享,因此精确的间隔是不确定的。
作为最后一个示例,你可能需要查看 pass4.rb 程序。这会创建两个线程并立即挂起它们(Thread.stop
)。在每个线程的主体中,线程的信息(包括其 object_id
)被附加到数组 arr
,然后调用 Thread.pass
。最后,运行并连接两个线程,并显示数组 arr
。尝试通过取消注释 Thread.pass
来验证其效果(密切注意其 object_id
标识符指示的线程的执行顺序):
arr = []
t1 = Thread.new{
Thread.stop
(1..10).each{
arr << Thread.current.to_s
Thread.pass
}
}
t2 = Thread.new{
Thread.stop
(1..10).each{ |i|
arr << Thread.current.to_s
Thread.pass
}
}
puts( "Starting threads..." )
t1.run
t2.run
t1.join
t2.join
puts( arr )