ruby线程和并发

线程和并发

线程生命周期

新线程可以通过Thread.new方法创建,也可以使用同义词方法Thread.start和Thread.fork。在创建线程后无需启动它,它将在CPU资源可用时自动启动。调用Thread.new会得到一个Thread对象,Thread类定义了很多方法,用于查询和操作运行的线程。

线程将运行Thread.new关联代码块中的代码,在运行完后停止。代码块中对吼一条语句的值成为线程的值,它可以通过Thread对象的value方法获得。如果一个线程已经运行完成,那么这个值被立刻传给thread的value方法,否则,value方法被阻塞,知道线程完成。

类方法Thread.current返回当前线程的thread对象,这样线程就可以对自己进行操作。类方法Thread.main返回代表主线程的Thread对象–这是ruby程序启动时的初始线程。

主线程

主线程有其特殊性:ruby解释器在主线程完成时会停止运行,即使在主线程创建的线程仍在运行时也是如此。因此,必须保证主线程在其他线程仍在运行时不会结束。一种方法是采用无限循环方式实现主线程,另一种方法是明确等待其他线程完成操作。前面已经提到可以对线程调用value方法来等待它结束,如果不关心线程的值,也可以使用join方法等待其结束。

下面方法将等待除主线程和当前线程(两者可能相同)外的所有线程结束:

def join_al
    main = Thread.main
    current = Thread.current
    all = Thread.list
    all.each {|t| t.join unless t == current or t == main}
end

下面程序将等新线程执行完继续执行:

t = Thread.new do
    sleep(10)
    puts 'thread finish'
end     
t.join #等待10秒
puts 'main thread finish'

线程和未处理的异常

如果在主线程中抛出异常并且没有被处理,ruby解释器将打印一条信息并退出。如果其他线程中有未处理的异常,那么只有这个线程被停止,在默认情况下,解释器不会打印消息或退出。如果线程t因为一个未处理的异常而中止,而另外一个线程s调用了t.join或t.value方法,那么t中产生的异常在线程s中被抛出。

如果希望所有线程的未处理异常都使解释器退出,你可以使用类方法:

Thread.abort_on_exception = true

如果希望某个特定线程的未处理异常使得解释器退出,可以使用同名的实例方法:

t = Thread.new {...}

t.abort_on_exception = true

线程局部变量

一些特殊的ruby全局变量是线程局部的,在不同的线程中它们可能有不同的值,比如$SAFE和$~就是这样。这意味着如果两个线程同时进行正则表达式匹配,它们看到的$~值是不同的,并且在一个线程上执行的匹配不会影响另一个线程的匹配操作。

Thread类提供了类似哈希表的行为。它定义了[]和[]=这两个实例方法,它们可以把符号关联到任意值上。(如果使用的事字符串,它们会被转换为一个符号,与真正的哈希表不同,Thread类只能用符号作为主键)这些符号所关联值的行为跟线程局部变量相似,它们不像代码块局部变量那样是私有的,任何变量都可以查询其他线程的相应的值,不过它们也不是共享变量,每个线程都有自己的拷贝。

Thread.current[:progress] = 100

除了[]和[]=方法,Thread还定义了key?方法来判断给定主键是否存在于一个线程中。keys方法返回一个符号数组,表示线程中定义的所有主键。

线程调度

优先级

影响线程调度的首要因素是线程优先级,高优先级的线程比低优先级的线程优先调度。更准确的说,一个线程只有在没有更高优先级的线程等待时才可能被CPU执行。

可以使用Thread对象的priority=和priority方法设置和查询线程的优先级。新创建线程的优先级与创建它的线程相同。主线程将以优先级0启动。

抢占式线程调度和Thread.pass方法

当多个同优先级线程须共享CPU时,线程调度器决定每个线程什么是偶执行及执行多久。一些调度器是抢占式的,这意味着每个同级别的线程都可以被执行固定的时间;另一种调度器是非抢占式的,一旦一个线程开始运行,除非它睡眠、IO阻塞或有更高优先级的线程醒来,否则它会一直运行下去。

耗时的计算绑定线程应该定期调用Thread.pass方法,可以让别的线程有机会获得CPU。

线程状态

一个ruby线程可能有5种状态。最值得关注的状态是活跃线程的:可能是可运行的(runnable),也可能是休眠状态(sleeping)。线程一般在可运行和休眠状态中来回切换。

Thread类定义了若干实例方法用于检测线程的状态。当线程出于可运行状态或休眠状态时,alive?方法返回true;当一个线程不出于可运行状态时,stop?方法返回true。status方法返回线程的状态。

状态值:

|线程状态|返回值|什么行为进入到此状态|
|——|——|—————-|
|可运行(runnable)|run|线程被创建时,线程出于可运行状态,可能被立刻执行|
|休眠(sleeping)|sleep|1、等待I/O操作休眠Kernel.sleep,2、被自身终止Thread.stop|
|正在中止(aborting)|aborting|过渡状态,一个被杀死的线程(Thread.kill)在还没有被终止前出于正在中止状态|
|正常退出(Terminated normally)|false|调用实例方法kill或terminate或exit|
|异常中止(Terminated with exception)|nil|raise抛出异常|

线程调用Thread.stop方法暂停执行,这将使线程进入休眠状态。这是个类方法,只对当前线程有效–没有对应的实力方法,因此不能强制其他线程暂停。调用Thread.stop的效果与调用无参数的Kernel.sleep相同:这个线程将被永远停止(或直到被唤醒)。

使用Thread.stop或Thread.sleep方法暂停的线程可以被实例方法wakeup和run重新激活(即使休眠时间还没到期),这两个方法都能让线程从休眠状态变为可运行状态。run方法还对调度器产生请求,让当前线程放弃对CPU的占用,这样新唤醒的线程就可能被立刻执行。wakeup方法只是唤醒指定线程,而不会让当前线程放弃CPU。

在线程所在代码块正常结束或异常中止时,线程从可运行状态切换到一种结束状态。另外一种正常结束线程的方式是调用Thread.exit方法,在这种方式下,注意在线程结束前所有ensure语句将被执行。

线程也可以被另外一个线程强行中止,这可以通过在被中止的线程上调用实例方法kill来实现。terminate和exit是kill的同义词方法。这些方法把杀死的线程置为正常退出状态,被杀死的线程在真正结束前运行所有ensure语句。而kill!方法(及同义词方法terminate!和exit!)在杀死线程前不允许执行任何ensure语句。

用实例方法raise抛出一个异常,如果这个线程不能处理引入的这个异常,它将进入异常中止状态。ensure语句被确保执行。

列举线程和线程组

Thread.list方法返回所有活跃线程(运行或休眠)的Thread对象数组。当一个线程结束时,它从这个数组中被删除。

如果希望给一组线程加入某种次序,可以创建一个ThreadGroup对象,并向其中加入线程:

group = ThreadGroup.new
3.times {|n| group.add(Thread.new {puts n;sleep})}

可以用实例方法group来查询一个线程所在的ThreadGroup。用ThreadGroup的list方法可以获得一个线程组中线程的数组(像Thread.list一样,ThreadGroup的实例方法list也只是返回那些没有中止的线程)。

group.list #[#<Thread:0x007fa7d35c9ce8 sleep>, #<Thread:0x007fa7d35c9bf8 sleep>, #<Thread:0x007fa7d35c9b30 sleep>]

group.list.first.group #<ThreadGroup:0x007fa7d3611480> 

ThreadGroup比一般线程数组更有用的地方在于它的enclose方法,一旦一个线程组被封闭(enclosed),既不能从中删除线程,也不能加入新的线程。

group.enclose
group.add Thread.new {puts 222;sleep} #ThreadError222: can't move to the enclosed thread group

线程示例

IO绑定程序是最经常使用线程的地方,线程可以让程序在等待用户输入、文件系统和网络时仍然继续工作。比如,下面定义一个conread方法,它接受一个文件名数组,返回一个哈希表,用于对应文件名和其他对应的文件内容。

require 'open-uri'
def conread(filenames)
    h = {}
    filenames.each do |filename|
      h[filename] = Thread.new do
        open(filename) {|f| f.read}
      end
    end
    h.each_pair do |filename, thread|
      begin
         h[filename] = thread.value
       rescue
         h[filename] = $!
      end
    end
end

线程互斥和死锁

如果两个线程共享某些数据,并且至少一个线程修改了这些数据,那就必须特别小心,保证不要让某个线程看到的数据出于不一致的状态。这被称为线程互斥,很多例子可表明其必要性。

比如两个线程同时对文件进行处理,每个线程都对一个共享变量进行加1操作,从而对打开文件的总数进行跟踪。问题在于对变量加1操作并非是原子操作,也就是说这个动作不能用一个步骤完成,ruby程序必须首先读出该变量的值,加上1,然后把新的值存回这个变量中。

对于这样的问题,需要用协作锁机制进行解决。每个希望访问共享数据的线程必须首先对数据进行加锁,锁用Mutex(是互斥–mutual exclusion的缩写)对象表示。要对一个Mutex对象加锁,可以调用它的lock方法,在读取或修改完共享变量时,再调用它的unlock方法。

如果对一个已经加锁的Mutex对象调用lock方法,在调用者成功获取一个锁之前,该方法会一直阻塞。如果每个线程都能正确对共享数据进行加锁和解锁操作,那么不会有线程看到数据处于不一致的状态,前面的那些问题就不会出现了。

通常我们并不显式调用lock和unlock方法,而是使用关联代码块的synchronize方法。synchronize方法会锁住Mutex,运行代码块中的代码,然后在一个ensure子句中对Mutex进行解锁,这样可以保证异常被恰当处理。

x = 0
mutex = Mutex.new
3.times.map do |i|
  Thread.new do
    mutex.synchronize do
      puts "before i: #{i}, #{x}"
      x += 1
      puts "after i: #{i}, #{x}"
    end
  end
end.each(&:join)

输出:

before i: 0, 0
after i: 0, 1
before i: 2, 1
after i: 2, 2
before i: 1, 2
after i: 1, 3

死锁

在使用Mutex对象进行线程互斥操作时必须小心避免死锁。在所有线程都等待获得其他线程持有的资源时,就会发生死锁,因为所有线程都被阻塞,它们不能释放所持有的锁,导致其他对象无法获得这些锁。

经典的死锁场景设计两个线程和两个Mutex对象。线程1锁住Mutex1,然后试图锁住Mutex2;同时,线程2锁住Mutex2并且试图锁住Mutex1。哪个线程都无法获得它们希望的锁,而且每个线程都不会释放另一个线程所需要的锁,因此两个线程将一直阻塞下去:

m,n = Mutex.new, Mutex.new

t = Thread.new {
  m.lock
  puts 'thread t locked Mutex m'
  sleep 1
  puts 'thread t waiting to lock Mutex n'
  n.lock
}

s = Thread.new {
  n.lock
  puts 'thread s locked Mutex n'
  sleep 1
  puts 'thread s waiting to lock Mutex m'
  m.lock
}

t.join
s.join

结果如下:
thread t locked Mutex m
thread s locked Mutex n
thread s waiting to lock Mutex mthread t waiting to lock Mutex n

index.rb:42:in `join': deadlock detected (fatal)

避免这种死锁的方法是一直按照相同顺序对资源进行加锁操作,如果第二个线程在加锁n之前锁住m,死锁就不会产生了:

m,n = Mutex.new, Mutex.new

t = Thread.new {
  m.lock
  puts 'thread t locked Mutex m'
  sleep 1
  puts 'thread t waiting to lock Mutex n'
  n.lock
}

s = Thread.new {
  m.lock
  puts 'thread s locked Mutex m'
  sleep 1
  puts 'thread s waiting to lock Mutex n'
  n.lock
  puts 'thread s locked Mutex n'
}

t.join
s.join

输出:
thread s locked Mutex m
thread s waiting to lock Mutex n
thread s locked Mutex n
thread t locked Mutex m
thread t waiting to lock Mutex n

注意,即使在不适用Mutex对象时,也可能发生死锁。如果某个线程对调用了Thread.stop的线程使用join方法,除非有第三个线程可以唤醒这个终止的线程,否则,这两个线程会同时死锁:

t = Thread.new {puts 111; sleep}

t.join #输出111,然后程序死锁,不动了

#另外一个线程唤醒t线程

t = Thread.new {puts 111; sleep}

Thread.new {sleep 10; t.wakeup}

t.join 

Queue和SizedQueue类

标准的线程库定义了专为并发编程设计的Queue和SizedQueue数据结构,它们实现了线程安全的先进先出队列,可以用于编写生产者/消费者模型的程序。使用这种模型,一个线程生产某种对象值并用enq(enqueue)或同义词方法push方法将之放入队列中,另外一个线程则“消费”这些对象值,可以根据需要用deq(dequeue)方法把它们从队列中移除。pop和shift方法是deq的同义词方法。

Queue适合于并发编程的关键特性在于当队列处在空状态时,deq方法会阻塞,直到生产者线程为队列增加一个对象值。Queue和SizedQueue实现相同的API,不过SizedQueue有最大长度限制。如果队列达到最大长度,那么增加对象值的方法会阻塞,直到消费者线程从队列中删除一个对象值。

可以用size活length方法确定队列元素的个数,也可以用empty?方法判断队列是否为空。在调用SizedQueue.new方法时,可以指定对象的最大长度,在创建SizedQueue对象之后,也可以用max=方法来修改它的最大长度。

下面这个方法用来计算枚举对象元素平方的和,不过要记得一个线性算法的速度肯定比下面这个球平方和的例子快:

module Enumerable
  def conject(initial, mapper, injector)
    q = Queue.new
    count = 0
    each do |item|
      Thread.new do
        q.enq(mapper[item])
      end
      count += 1
    end
    t = Thread.new do
      x = initial
      while(count > 0)
        x = injector[x, q.deq]
        count -= 1
      end
      x
    end
    t.value
  end
end

a = [1,2,3]
mapper = lambda {|x| x*x} #求平方的代码块
injector = lambda {|total, val| total+val}
puts a.conject(0, mapper, injector) #输出14

puts a.inject(0) {|total, x| total + x*x} #这种更快

条件变量和队列

对于Queue对象,有一个重要事项需要注意:deq方法会阻塞。对于Queue类来说,如果队列为空,一个消费者线程必须等待,直到一个生产者线程调用enq方法让队列不为空。

使用ContitionVariable,能以最清晰的方式让一个线程保持等待,直到其他线程通知它可以再次执行为止。像Queue一样,ConditionVariable也是标准线程库的一部分,可以用ConditionVariable.new方法创建一个ConditionVariable对象。用wait方法可以让一个线程等待这个条件;用signal方法可以唤醒一个等待线程;用broadcast可以唤醒所有等待线程。使用条件变量有一个小技巧:为了让一切正常工作,等待线程必须给wait方法传递一个上锁的Mutex对象。在这个线程等待过程中,这个Mutex对象会暂时解锁,而在线程唤醒后重新上锁。

mutex = Mutex.new

cv = ConditionVariable.new
a = Thread.new{
  mutex.synchronize{
    puts 'A: i have section, but will wait for cv'
    cv.wait(mutex)
    puts 'A: i have section again!'
  }
}

puts 'later, back at the ranch'

b = Thread.new{
  mutex.synchronize{
    puts 'B: i am donw with cv'
    cv.signal
    puts 'B: i am finishing up'
  }
}

a.join
b.join

将输出如下:

A: i have section, but will wait for cv
later, back at the ranch
B: i am donw with cv
B: i am finishing up
A: i have section again!

你可能感兴趣的:(线程,并发)