互斥

有时两个或多个线程可能都需要访问某种全局资源。由于全局资源的当前状态可能被一个线程修改,并且该修改的值在被某个其它线程使用时可能是不可预测的,因此这可能产生错误的结果。举一个简单的例子,看看这段代码:

no_mutex.rb
  1. $i = 0
  2. a = Thread.new {
  3. 1000000.times{ $i += 1 }
  4. }
  5. b = Thread.new {
  6. 1000000.times{ $i += 1 }
  7. }
  8. a.join
  9. b.join
  10. puts( $i )

我的目的是运行两个线程,每个线程递增全局变量 $i 一百万次。在这结束时 $i 的预期结果(自然)将是 200 万。但是,事实上,当我运行它时,$i 的结束值是 1088237(你可能会看到不同的结果)。

对此的解释是,这两个线程实际上正在竞争对全局变量 $i 的访问。这意味着,在某些时候,线程 a 可能获得 $i 的当前值(假设它恰好是 100)并且同时线程 b 也获得 $i 的当前值(仍然是 100)。现在,a 增加它刚刚获得的值($i 变为 101)并且 b 增加它刚刚获得的值(因此 $i 再次变为 101!)。换句话说,当多个线程同时访问共享资源时,其中一些线程可能正在使用过时的值 - 即,没有考虑其它线程对该资源所做的任何修改。随着时间的推移,这些操作产生的错误会累积,直到我们得出的结果与我们预期的结果大不相同。

为了解决这个问题,我们需要确保当一个线程可以访问全局资源时,它会阻止其它线程的访问。另一种说法,即授予多个线程对全局资源的访问应该是“互斥的”(mutually exclusive)。你可以使用 Ruby 的 Mutex 类来实现它,该类使用信号量来指示当前资源是否正在被访问,并提供同步方法以防止(外部)访问块内的资源。请注意,你必须引入(require)‘thread’ 才能使用 Mutex 类。这是我重写的代码:

mutex.rb
  1. require 'thread'
  2. $i = 0
  3. semaphore = Mutex.new
  4. a = Thread.new {
  5. semaphore.synchronize{
  6. 1000000.times{ $i += 1 }
  7. }
  8. }
  9. b = Thread.new {
  10. semaphore.synchronize{
  11. 1000000.times{ $i += 1 }
  12. }
  13. }
  14. a.join
  15. b.join
  16. puts( $i )

这次,$i 的最终结果是 2000000。

最后,有关使用 Threads 的更有用的示例,请查看 file_find2.rb。此示例程序使用 Ruby 的 Find 类遍历磁盘上的目录。有关非线程示例,请参阅 file_find.rb。将其与第 13 章中的 file_info3.rb 程序进行比较,其使用的 Dir 类。

这会设置两个线程运行。第一个,t1,调用 processFiles 方法来查找和显示文件信息(你需要编辑对 processFiles 的调用以将系统上的目录名传递给它)。第二个线程 t2 只打印出一条消息,当 t1 处于“活着”状态(即运行或休眠)时,该线程运行:

file_find2.rb
  1. t1 = Thread.new{
  2. Thread.stop
  3. processFiles( '..' ) # edit this directory name
  4. }
  5. t2 = Thread.new{
  6. Thread.stop
  7. while t1.alive? do
  8. print( "\n\t\tProcessing..." )
  9. Thread.pass
  10. end
  11. }

每个线程使用 Thread.pass 完成让步控制(t1 线程在 processFiles 方法内进行让步控制)。在实际应用程序中,你可以采用此技术以提供某种类型的用户反馈,同时进行一些密集的过程(例如目录遍历)。