Ruby 线程(二)
6、Using a Thread Group
线程组是管理线程的一种方式,它将线程彼此从逻辑上关联起来。通常,所有线程属于Default线程组(它是个类常量)。但如果创建了一个新线程组,则新线程会被添加到其中。
一个线程每次只可属于一个线程组。当线程被添加到线程组时,它自动地被从它先前的线程组中移出。
ThreadGroup.new类方法创建一个新线程组,然后adds实例方法添加线程到组内:
f1thread = Thread.new("file1") { |file| waitfor(file) }
f2thread = Thread.new("file2") { |file| waitfor(file) }
file_threads = ThreadGroup.new
file_threads.add f1
file_threads.add f2
# Count living threads in this_group
count = 0
this_group.list.each { |x| count += 1 if x.alive? }
if count < this_group.list.size
puts "Some threads in this_group are not living."
else
puts "All threads in this_group are alive."
end
有很多有用的方法被添加给ThreadGroup。这儿我们显示的方法唤醒组内的每个线程,等待捕获所有线程(通过join),杀死组内所有线程:
class ThreadGroup
def wakeup
list.each { |t| t.wakeup }
end
def join
list.each { |t| t.join if t != Thread.current }
end
def kill
list.each { |t| t.kill }
end
二、Synchronizing Threads
为什么同步是必须的?这是因为操作的交错引起变量和其它实体,在不明显地被从不同线程的读代码的访问方式。两个或更多线程访问同一变量可以彼此互相影响,这种方式是无法预料和调试困难的。
让我们看看这个例子的代码片断:
x = 0
t1 = Thread.new do
1.upto(1000) do
x = x + 1
end
end
t2 = Thread.new do
1.upto(1000) do
x = x + 1
end
end
t1.join
t2.join
puts x
变量x开始时为0。每个线程1000秒增加它一次。逻辑告诉我们输出时x必须是2000。
但是我们这儿有什么?在一个特定系统上,它打印1044做为结果。哪儿有错误?
我们代码假设一个整数的增加操作是原子的(或不可分割的)操作。但是它不是。考虑下面逻辑流程。我们放置线程t1在左边,t2在右边。每个行是一个单独的时间片,我们假设在进入这个逻辑片时,x的值是123。
t1 线程 t2 线程
__________________________ __________________________
获取x的值(123)
获取x的值 (123)
将值加1 (124)
将值加1 (124)
存储结果到x内
存储结果到x内。
很明显,每个线程都从它自己的视点来完成简单的增量操作。这种情况下,同样明显的是在两个线程执行完增量后x只有124。
这只是简单的同步问题。最坏的部分会变得更难于管理,并且成为计算机学家和数学家研究的真正对象。
简单的同步形式是使用临界区。当线程进入代码的临界区时,这个技术保证没有其它线程将被运行直到第一个线程离开它的临界区。
Thread.critical存取器,当设置为true时,将阻止其它线程被调度。这儿们看个例子,我们只讨论和使用这个技术来修正它。
x = 0
t1 = Thread.new do
1.upto(1000) do
Thread.critical = true
x = x + 1
Thread.critical = false
end
end
t2 = Thread.new do
1.upto(1000) do
Thread.critical = true
x = x + 1
Thread.critical = false
end
end
t1.join
t2.join
puts x
现在逻辑流程被强迫成类似下面。(当然,在增量部分的外面,线程是自由地或多或少随机地交错操作。)
t1 线程 t2 线程
__________________________ __________________________
取出x的值(123)
增量操作(124)
存储结果回x内
取出x的值 (124)
增量操作(125)
存储结果回x内
线程管理和完成操作的结合是可能的,这会引起一个线程被调度,即使另一个线程在临界区中。在最简单情况下,新创建的线程将立即运行,而不管另一个线程是否在临界区中。因此,这个技术应该只被用在最简单的环境中。
让我们拿一个Web索引应用程序做为例子。我们在网络上的多个源中取出单词并且存储它们到一个哈希表内。单词本身将被做为键,而值是识别文档及文档内行号的字符串。
这是个非常粗糙的例子。但是出于简单的理由我们让它更粗糙:
1. 我们将远程文档描述成简单字符串。
2. 我们将它限制为三个字符串(简单的硬编码数据)。
3. 我们用随机睡眠模仿网络访问的变化。
那么,让我们来看看Listing7.1。它甚至不打印它收集的数据,并且只有一个被找到单词数的count。注意每当哈希表被检查或更改时,我们调用hesitate方法来睡眠随机间隔。这会让程序运行在更不确定和更现实的方式上。
Listing 7.1 Flawed Indexing Example (with a Race Condition)
$list = []
$list[0]="shoes shipsnsealing-wax"
$list[1]="cabbages kings"
$list[2]="quarksnshipsncabbages"
def hesitate
sleep rand(0)
end
$hash = {}
def process_list(listnum)
lnum = 0
$list[listnum].each do |line|
words = line.chomp.split
words.each do |w|
hesitate
if $hash[w]
hesitate
$hash[w] += ["#{ listnum} :#{ lnum} "]
else
hesitate
$hash[w] = ["#{ listnum{ :#{ lnum} "]
end
lnum += 1
end
end
t1 = Thread.new(0) { |list| process_list(list) }
t2 = Thread.new(1) { |list| process_list(list) }
t3 = Thread.new(2) { |list| process_list(list) }
t1.join
t2.join
t3.join
count = 0
$hash.values.each do |v|
count += v.size
end
puts "Total: #{ count} words" # May print 7 or 8!
但是有个问题。如果你的系统行为与我们的一样,这儿是程序可能输出的两个数字!在我们的测试中,它近似相等地打印7和8。在有更多单词的情况下,会有更大的变化。
让我们试试用互斥来修正它,互斥用于控制共享资源的访问。(当然,这个术语来自于单词mutual exclusion。)Mutex库将允许我们创建和操纵一个mutex。当我们准备访问哈希表时,我们可以锁住它,当我们完成时,我们解锁它(参见Listing7.2)。
Listing 7.2 Mutex Protected Indexing Example
require "thread.rb"
$list = []
$list[0]="shoes shipsnsealing-wax"
$list[1]="cabbages kings"
$list[2]="quarksnshipsncabbages"
def hesitate
sleep rand(0)
end
$hash = {}
$mutex = Mutex.new
def process_list(listnum)
lnum = 0
$list[listnum].each do |line|
words = line.chomp.split
words.each do |w|
hesitate
$mutex.lock
if $hash[w]
hesitate
$hash[w] += ["#{ listnum} :#{ lnum} "]
else
hesitate
$hash[w] = ["#{ listnum{ :#{ lnum} "]
end
$mutex.unlock
end
lnum += 1
end
t1 = Thread.new(0) { |list| process_list(list) }
t2 = Thread.new(1) { |list| process_list(list) }
t3 = Thread.new(2) { |list| process_list(list) }
t1.join
t2.join
t3.join
count = 0
$hash.values.each do |v|
count += v.size
end
puts "Total: #{ count} words" # Always prints 8!
我们应该提一下除了lock外,Mutex类也有try_lock方法。它的行为类似于lock,除了当另一个线程已经锁时,它将直接返回false而不等待。
$mutex = Mutex.new
t1 = Thread.new { $mutex.lock; sleep 30 }
sleep 1
t2 = Thread.new do
if $mutex.try_lock
puts "Locked it"
else
puts "Could not lock" # Prints immediately
end
end
sleep 2
线程库thread.rb有几个有时很有用的类。类Queue是同步访问队列末端的线程敏感的队列;也就是说,不同的线程可以使用同一队列,而不会出现问题。SizedQueue类本质一样的,除了它允许限制队列的大小(队列可包含的元素数量)。
有很多相似的方法,因为SizedQueue实际上继承了Queue。导出类也有存取器max来用或set队列最大尺寸。
buff = SizedQueue.new(25)
upper1 = buff.max # 25
# Now raise it...
buff.max = 50
upper2 = buff.max # 50
Listing7.3显示了一个简单的生产者-消费者演示。消费者以平均时间(通过一个很长的睡眠时间)被显示,以便条目的收集。
Listing 7.3 The Producer-Consumer Problem
require "thread"
buffer = SizedQueue.new(2)
producer = Thread.new do
item = 0
loop do
sleep rand 0
puts "Producer makes #{ item} "
buffer.enq item
item += 1
end
end
consumer = Thread.new do
loop do
sleep (rand 0)+0.9
item = buffer.deq
puts "Consumer retrieves #{ item} "
puts " waiting = #{ buffer.num_waiting} "
end
end
sleep 60 # Run a minute, then die and kill threads
方法enq和deq是用于获取队列的进出条目的被推荐方式。我们也可以使用push来添加条目到队列,用pop或shift从队列移出条目,但是当我们明确使用队列时,这些名字有点缺少记忆价值。
方法empty?测试空队列,clear方法清空队列。方法size(别名length)返回队列内实际条目数量。
# Assume no other threads interfering...
buff = Queue.new
buff.enq "one"
buff.enq "two"
buff.enq "three"
n1 = buff.size # 3
flag1 = buff.empty? # false
buff.clear
n2 = buff.size # 0
flag2 = buff.empty? # true
num_waiting方法是等待访问队列的线程数量。在没有指定大小的队列中,是等待移除元素的线程的数量;在指定大小的队列中,同样是等待添加元素到队列的线程数量。
Queue类内的deq方法有可选参数non_block,缺省值是false。如果它为true,一个空队列会产生ThreadError错误而不是锁住线程。
And he called for his fiddlers three.
"Old King Cole" (traditional folk tune)
条件变量是个真正的线程队列。它与mutex一同使用,在同步线程时提供高级别的控制。