深入探索

传递执行权给其它线程

在某些情况下,你可能特别希望某个线程(thread)能够让步执行权(execution)给任何其它线程以让其运行。例如,如果你有多个线程正在进行稳定的更新图形操作或显示各种“正在发生的”统计信息,你可能需要确保一旦一个线程绘制了 X 个像素或显示了 Y 个统计数据,另一个线程保证有机会做一些其它事情。

从理论上讲,Thread.pass 方法可以解决这个问题。根据 Ruby 的源代码文档,Thread.pass 调用线程调度程序将执行权传递给另一个线程。这是 Ruby 文档提供的示例:

pass0.rb
  1. a = Thread.new { print "a"; Thread.pass;
  2. print "b"; Thread.pass;
  3. print "c" }
  4. b = Thread.new { print "x"; Thread.pass;
  5. print "y"; Thread.pass;
  6. print "z" }
  7. a.join
  8. b.join

根据文档,此代码在运行时会生成以下输出:

  1. axbycz

是的,确实如此。理论上,这似乎表明,通过在每次调用 print 之后调用 Thread.pass,这些线程将执行权传递给另一个线程,这就是两个线程的输出交替的原因。

出于我心中的疑问,我想知道 Thread.pass 的调用被删除后会产生什么影响?第一个线程是否会一直占用,只有在结束后才让步于第二个线程?找出答案的最佳方法是尝试:

pass1.rb
  1. a = Thread.new { print "a";
  2. print "b";
  3. print "c" }
  4. b = Thread.new { print "x";
  5. print "y";
  6. print "z" }
  7. a.join
  8. b.join

如果我的理论是正确的(该线程将一直占用,直到它完成),这将是预期的输出:

  1. abcdef

事实上,(令我惊讶的是!),实际产生的输出是:

  1. axbycz

换句话说,无论是否调用 Thread.pass,结果都是相同的。那么,Thread.pass 做什么呢?其宣称 pass方法,调用线程调度程序将执行权传递给另一个线程,该文档是错误的吗?

在一个短暂而愤怒的时刻,我承认我轻率的认为有一种可能性,文档是不正确的,并且 Thread.pass 根本没有做任何事情。深入研究 Ruby 的 C 语言源代码很快消除了我的疑虑;Thread.pass 确实做了一些事情,但它的行为并不像 Ruby 文档暗示的那样可预测。在解释原因之前,让我们尝试一下我自己的示例:

pass2.rb
  1. s = 'start '
  2. a = Thread.new { (1..10).each{
  3. s << 'a'
  4. Thread.pass
  5. }
  6. }
  7. b = Thread.new { (1..10).each{
  8. s << 'b'
  9. Thread.pass
  10. }
  11. }
  12. a.join
  13. b.join
  14. puts( "#{s} end" )

乍一看,这可能看起来与前面的示例非常相似。它设置两个线程运行,但不是反复打印东西出来,而是重复地将一个字符附加到字符串中 - ‘a’ 由线程 a 添加,’b’ 由线程 b 添加。每次操作后,Thread.pass 将执行权传递给另一个线程。最后显示整个字符串。字符串包含 ‘a’ 和 ‘b’ 的交替序列应该不足为奇:

  1. abababababababababab

现在,请记住,在上一个程序中,即使我删除了对 Thread.pass 的调用,我也获得了完全相同的交替输出。基于这种经历,如果我在这个程序中删除 Thread.pass,我想我应该期望得到类似的结果。我们来试试吧:

pass3.rb
  1. s = 'start '
  2. a = Thread.new { (1..10).each{
  3. s << 'a'
  4. }
  5. }
  6. b = Thread.new { (1..10).each{
  7. s << 'b'
  8. }
  9. }
  10. a.join
  11. b.join
  12. puts( "#{s} end" )

这次,输出如下:

  1. aaaaaaaaaabbbbbbbbbb

换句话说,这个程序显示了我最初在第一个程序中预料的那种不同的行为(我从 Ruby 的嵌入式文档中复制出来的那个)- 也就是说当两个线程在它们自己的时间片下运行时,第一个线程,a,抢占所有时间为它自己所用,只有当它完成时第二个线程 b 才会得到关注。但是通过显式添加对 Thread.pass 的调用,我们可以强制每个线程将执行权传递给任何其它线程。

那么我们如何解释这种行为上的差异呢?从本质上讲,pass0.rbpass3.rb 正在做同样的事情 - 运行两个线程并显示每个线程的字符串。唯一真正的区别在于,在 pass3.rb 中,字符串在线程内连接而不是打印。这可能看起来不是什么大不了的事,但事实证明,打印字符串比连接字符串需要更多的时间。实际上,print 调用会引入时间延迟。正如我们之前发现的那样(当我们有意使用 sleep 引入延迟时),时间延迟对线程产生了深远的影响。

如果你仍然不相信,请尝试我重写的 pass0.rb 版本,我创造性地命名为 pass0_new.rb。这只是用连接替换了打印。现在,如果你对 Thread.pass 的调用进行注释和取消注释,你确实会看到不同的结果。

pass0_new.rb
  1. s = ""
  2. a = Thread.new { s << "a"; Thread.pass;
  3. s << "b"; Thread.pass;
  4. s << "c" }
  5. b = Thread.new { s << "x"; Thread.pass;
  6. s << "y"; Thread.pass;
  7. s << "z" }
  8. a.join
  9. b.join
  10. puts( s )

顺便说一句,我的测试是在运行 Windows 的 PC 上进行的。很可能在其它操作系统上会看到不同的结果。这是因为控制分配给线程的时间量的 Ruby 调度程序的实现在 Windows 和其它操作系统上是不同的。在 Unix 上,调度程序每 10 毫秒运行一次,但在 Windows 上,通过在某些操作发生时递减计数器来控制时间共享,因此精确的间隔是不确定的。

作为最后一个示例,你可能需要查看 pass4.rb 程序。这会创建两个线程并立即挂起它们(Thread.stop)。在每个线程的主体中,线程的信息(包括其 object_id)被附加到数组 arr,然后调用 Thread.pass。最后,运行并连接两个线程,并显示数组 arr。尝试通过取消注释 Thread.pass 来验证其效果(密切注意其 object_id 标识符指示的线程的执行顺序):

pass4.rb
  1. arr = []
  2. t1 = Thread.new{
  3. Thread.stop
  4. (1..10).each{
  5. arr << Thread.current.to_s
  6. Thread.pass
  7. }
  8. }
  9. t2 = Thread.new{
  10. Thread.stop
  11. (1..10).each{ |i|
  12. arr << Thread.current.to_s
  13. Thread.pass
  14. }
  15. }
  16. puts( "Starting threads..." )
  17. t1.run
  18. t2.run
  19. t1.join
  20. t2.join
  21. puts( arr )