1. 创建线程

可以使用Thread.new调用来创建新线程,为它提供一个含有将要在新线程中运行的block,例如:

require 'net/http'
pages = %w(www.baidu.com www.google.com)
threads = []
for page_to_fetch in pages
  threads << Thread.new(page_to_fetch) do |url|
    h = Net::HTTP.new(url, 80)
    puts "Fetching: ${url}"
    resp = h.get('/', nil)
    puts "Got #{url}: #{resp.message}"
  end
end
threads.each {|thr| thr.join}

线程共享在它开始执行时就已经存在的所有全局变量、实例变量和局部变量。当第一个线程运行时,page_to_fetch被设置为"www.baidu.com",同时创建线程的循环仍然运行,第二次时,page_to_fetch被设置为"www.google.com"。如果使用page_to_fetch变量的第一个线程还没结束,它会突然开始使用新的值。不过,在线程block里创建的局部变量对线程来说是真正的局部变量,每个线程会有这些变量的私有备份。

当Ruby程序终止时,不管线程状态如何,所有线程都被杀死。可以通过调用Thread#join方法来等待特定线程的正常结束。如果不想永远阻塞,可以给予join一个超时参数:如果超时在线程终止之前到期,join返回nil。join的另一个变种,Thread#value方法返回线程执行的最后语句的值。

除了join之外,还有一些便利函数。使用Thread.current总是可以得到当前线程,可以使用Thread.list得到一个所有线程的列表,可以使用Thread#status和Thread#alive?去确定特定线程的状态,另外,可以使用Thread#priority=调整线程的优先级。

如果需要线程局部变量能被别的线程访问,Thread类提供了一种特别措施,允许通过名字来创建和访问线程局部变量,例如:

count = 0
threads = []
10.times do |i|
  threads[i] = Thread.new.do
    Thread.current["mycount"] = count
    count += 1
  end
end
threads.each {|t| t.join; print t["mycount"], ","}
puts "count = #{count}"

如果线程引发了末处理的异常,它的反应依赖于abort_on_exception标志和解释器debug标志。如果abort_on_exception是false,debug标志没有启用(默认),未处理的异常会简单杀死当前线程,而其他线程继续运行。如果设置abort_on_exception为true,或使用-d选项去打开debug标志,未处理的异常会杀死所有正在运行的线程。


2. 线程调度器

Thread类提供了若干种控制线程调度器的方法。调用Thread.stop停止当前线程,调用Thread#run安排运行特定的线程。Thread.pass把当前线程调度出去,允许运行别的线程,Thread#join和Thread#value挂起调用它们的线程,直到指定的线程结束为止,例如:

class Chaser
  attr_reader :count
  def initialize(name)
    @name = name
    @count = 0
  end
  def chase(other)
    while @count < 5
      while @count-other.count > 1
        Thread.pass
      end
      @count += 1
      print "#@name: #{count}\n"
    end
  end
end
c1 = Chaser.new("A")
c2 = Chaser.new("B")
threads = [
  Thread.new{Thread.stop; c1.chase(c2)},
  Thread.new{Thread.stop; c2.chase(c1)}
]
start_index = rand(2)
threads[start_index].run
threads[1-start_index].run
threads.each {|t| t.join}


3. 互斥

最底层的阻止其他线程运行的方法是使用全局的线程关键条件,当条件被设置为true(使用Thread.critical=)时,调度器将不会调度现有的线程去运行。但是这不会阻止创建和运行新线程。使用Thread.critical=不是很方便,Ruby有多种变通方法。

监视器用同步函数对包含一些资源的对象进行了封装,例如:

require 'monitor'
class Counter < Monitor
  attr_reade :count
  def initialize
    @count = 0
    super
  end
  def tick
    synchronize do
      @count +=1
    end
  end
end
c = Counter.new
t1 = Thread.new{100.times{c.tick}}
t2 = Thread.new{100.times{c.tick}}
t1.join
t2.join

通于特定的监视器对象,每次只有一个线程能够执行synchronize block中的代码,所以再也不会有两个线程同时缓存中间结果。

线程库中的Queue类实现了一个线程安全的队列机制,多个线程可以添加和从队列中删除对象,保证了每次添加和删除是原子性的操作。


4. 多进程

有好几种方式可以衍生单独的进程,最简单的方式是运行一些命令,然后等待它结束。Ruby使用system命令和反引号方法来实现它,例如:

system("tar xzf test.tgz")
result = `date`

Kernel.system方法在子进程中执行给定的命令,如果命令被找到和正确执行了,它会返回true,否则返回false。如果失败了,子进程的退出码保存在全局变量$?中。

很多情况下需要更多的控制:跟子进程进行对话,发送数据给它和从它那儿得到数据。IO.popen方法可以这么做。popen方法在子进程里运行命令,把子进程的标准输入和标准输出连接到Ruby IO对象,例如:

pig = IO.popen("/usr/local/bin/pig", "w+")
pig.puts "ice cream after they go to bed"
pig.close_write
puts pig.gets

popen还有另一个值得注意的地方,如果被传递的命令是单个减号"-",popen会创建新的Ruby解释器,这个解释器和原先的解释器在popen返回后都会继续运行。原先的进程会接受到IO对象,而子进程得到nil,例如:

pipe = IO.popen("-", "w+")
if pipe
  pipe.puts "Get a job!"
  STDERR.puts "Child says '#{pipe.gets.chomp}'"
else
  STDERR.puts "Dad says '#{gets.chomp}'"
  puts "OK"
end

kernel.fork调用会在父进程中返回进程ID,在子进程中返回nil,例如:

exec("sort testfile > output.txt") if fork.nil?
Process.wait

调用Process.wait可以等待sort结束,同时会返回它的进程ID。如果需要在子进程结束时收到通知而不是等待,可以使用Kernel.trap设置信号处理方法,例如:

trap("CLD") do
  pid = Process.Wait
  puts "Child pid #{pid}: terminated"
end
exec("sort testfile > output.txt") if fork.nil?