Ruby 线程(三)
5、使用其它同步技术
另一个同步机制是监视器,Ruby在monitor.rb库中实现。这个技术比互斥要更高级;特别是互斥锁不可以被嵌套,但监听器锁可以。
有些琐细的事从未发生过。那是因为没人会像下面这样写:
$mutex = Mutex.new
$mutex.synchronize do
$mutex.synchronize do
#...
end
end
但是它也许会发生(或通过一个递归调用)。在任何这些情况下的结果是死锁。避免这种情形下的死锁是混插Monitor的优势。
$mutex = Mutex.new
def some_method
$mutex.synchronize do
#...
some_other_method # Deadlock!
end
end
def some_other_method
$mutex.synchronize do
#...
end
end
Monitor mixin被典型地用于扩展任何对象。new_cond方法可以用于实例化一个条件变量。
ConditionVariable类来自于第三方库是对monitor.rb的增强。它有方法wait_until和wait_while,它的块是基于条件的线程。 which block a thread based on a condition.它也允许在等待时的暂停,因为wait方法有个timeout参数,它是个秒数(缺省为nil)。
因为我们快速地用完线程的例子,我们拿出Listing7.5内使用监视器技术重写Queue和SizedQueue类。代码由Shugo Maeda写出,并被许可使用。
Listing 7.5 Implementing a Queue with a Monitor
# Author: Shugo Maeda
require "monitor"
class Queue
def initialize
@que = []
@monitor = Monitor.new
@empty_cond = @monitor.new_cond
end
def enq(obj)
@monitor.synchronize do
@que.push(obj)
@empty_cond.signal
end
end
def deq
@monitor.synchronize do
while @que.empty?
@empty_cond.wait
end
end
end
end
class SizedQueue < Queue
attr :max
def initialize(max)
super()
@max = max
@full_cond = @monitor.new_cond
end
def enq(obj)
@monitor.synchronize do
while @que.length >= @max
@full_cond.wait
end
super(obj)
end
end
def deq
@monitor.synchronize do
obj = super
if @que.length < @max
@full_cond.signal
return obj
end
end
def max=(max)
@monitor.synchronize do
@max = max
@full_cond.broadcast
end
end
sync.rb 库以更多方式来完成线程的步。对于我们知道和关心的事情,它用一个counter实现了二相锁。在写本书时,它的唯一文档就是库本身。
6、Allowing Timeout of an Operation
在很多情况下,我们需要有允许完成一个动作的最长时间。这可避免死循环并允许在处理上有额外的控制层。这对网络环境下是个有用的特征,在其它环境下,我们可能或者不可能从远程服务器上得到响应。
timeout.rb库以基于线程的解决办法处理这个问题。Timeout方法执行写方法调用关联的块;当到达指定秒数时,它抛出TimeoutError错误,可以用rescue子句捕获它(见Listing7.6)。
require "timeout.rb"
flag = false
begin
timeout(5) do
puts "I want a cookie!"
answer = gets.chomp
flag = true
end
rescue TimeoutError
flag = false
end
if flag
if answer == "cookie"
puts "Thank you! Chomp, chomp, ..."
else
puts "That's not a cookie!"
exit
end
else
puts "Hey, too slow!"
exit
end
7、等待事件
在很多情况下,我们想在其它线程完成其它事情时,从外部监视一个或多个线程。这儿例子是人为的,但它演示通用原则。
这儿,们看到三个线程完成一个应用程序的工作。另一个线程每五秒被简单地唤醒,检查全局变量$flag,当它看到这个flag设置时,它唤醒其它三个线程。它保存三个工作线程直接与其它二个线程交互,并且试图唤醒它们。
$job = false
work1 = Thread.new { job1() }
work2 = Thread.new { job2() }
work3 = Thread.new { job3() }
thread5 = Thread.new { Thread.stop; job4() }
thread6 = Thread.new { Thread.stop; job5() }
watcher = Thread.new do
loop do
sleep 5
if $flag
thread5.wakeup
thread6.wakeup
Thread.exit
end
end
end
在job方法的运行期间任何时候若变量$flag变成true,thread5和thread6保证在五秒内启动。在这之后,watcher线程终止。
下个例子中,我们等待文件被创建。我们每三十秒检查一次,如果看见了就启动另一个线程;在此期间,其它线程可以做任何事。实际上,这儿我们在分别在观察三个文件。
def waitfor(filename)
loop do
if File.exist? filename
file_processor = Thread.new do
process_file(filename)
end
Thread.exit
else
sleep 30
end
end
end
waiter1 = Thread.new { waitfor("Godot") }
sleep 10
waiter2 = Thread.new { waitfor("Guffman") }
sleep 10
headwaiter = Thread.new { waitfor("head") }
# Main thread goes off to do other things...
还有很多其它情况,线程会等待一个外部事件 ,如网络应用程序,服务器端的socket慢或不可靠。
8、Continuing Processing During I/O
一个应用程序经常地有一个或多个冗长的或费时I/O操作。在有用户输入的情况下就会这样,因为用户在键盘上的输入甚至比磁盘操作都很慢。我们可以通过线程来使用这段时间。
考虑国际像棋的例子,它必须等待人移动它。当然,我们只表示这个概念的框架。
我们假设迭代器predictMove将重复地生成相似的人们可能做出的移动(然后确定程序员对这些移动的自己的响应)。然后当人们移动时,它可能已经准备好预期的移动了。
scenario = { } # move-response hash
humans_turn = true
thinking_ahead = Thread.new(board) do
predictMove do |m|
scenario[m] = myResponse(board,m)
Thread.exit if humans_turn == false
end
end
human_move = getHumanMove(board)
humans_turn = false # Stop the thread gracefully
# Now we can access scenario which may contain the
# move the person just made...
我们必须做出声明,真正的像棋程序通常不使用这种方式工作。通常关心的是快速搜索和通过一定的深度;在现实生活中,最好的解决办法是在thinking线程期间存储获取的部分状态信息,然后以同样方式继续直到程序找到一个好的响应或超出它的时间。
假设你想在超过一个的对象上并行迭代。也就是说,n个对象中的每一个,你想迭代第一个,第二个,第三个等等。
为做得更具体一些,看看下面例子。这儿我们假设compose是提供迭代器组成的方法的名字。我们也假设每个特定对象有个被使用的缺省迭代器each,每个对象每次提出个条目。
arr1 = [1, 2, 3, 4]
arr2 = [5, 10, 15, 20]
compose(arr1, arr2) do |a,b|
puts "#{ a} and #{ b} "
end
# Should output:
# 1 and 5
# 2 and 10
# 3 and 15
# 4 and 20
我们能采用更有思想的方式,在每个对象上完成迭代,一个接一个,存储结果。但是如果我们想要更优美的解决办法,实际上是不存储所有条目,线程是唯一容易的解决办法。我们的答案在Listing7.7中。
Listing 7.7 Iterating in Parallel
def compose(*objects)
threads = []
for obj in objects do
threads << Thread.new(obj) do |myobj|
me = Thread.current
me[:queue] = []
myobj.each do |element|
me[:queue].push element
end
end
end
list = [0] # Dummy non-nil value
while list.nitems > 0 do # Still some non-nils
list = []
for thr in threads
list << thr[:queue].shift # Remove one from each
end
yield list if list.nitems > 0 # Don't yield all nils
end
end
x = [1, 2, 3, 4, 5, 6, 7, 8]
y = " firstn secondn thirdn fourthn fifthn"
z = %w[a b c d e f]
p [a, b, c]
end
# Output:
#
# [1, " firstn", "a"]
# [2, " secondn", "b"]
# [3, " thirdn", "c"]
# [4, " fourthn", "d"]
# [5, " fifthn", "e"]
# [6, nil, "f"]
# [7, nil, nil]
注意我们没有假设所有对象在迭代时有相同数量的条目。如果一个迭代器在其它迭代器之前运行完,它将生成nil值直到最长的迭代器运行完毕。
当然,可以写更通用的方法来从每个迭代器中抓取多于一个的值。(毕竟,不是所有迭代器都每次只返回一个值。)我们可以让第一个参数指定每次迭代的数量。
使用任意迭代器(而不是缺省的each)也是可行的。我们可以传递它们的名字做为字符串,并使用send来调用它们。当然这需要其它窍门来完成。
然而,我们认为这儿给出的例子对大多数情形足够了。我们将其它的变化留给你做为练习。
只是出于乐趣,让我们使用第四章中的"External Data Manipulation"的例子并且并行化它。(没有,我们不是说平行地使用多个处理器。)这儿以线程形式给出递归删除例程。当我们找到的目录条目本身是个目录时,我们启动新线程来遍历那个目录并删除它的内容。
我们保存我们创建的线程的跟踪在称为threads的数组内;因为它是局部变量,每个线程都有它自己的那个数组的拷贝。它每次只可以由一个线程访问,这儿不需要对它同步访问。
注意我们也传递全文件名给线程块,以便我们不必为线程访问了一个可修改的值而烦心。线程使用fn做为同一变量的本地拷贝。
当我们遍历一个目录时,我们想在删除我们已完成工作的目录之前等待我们创建的线程。
def delete_all(dir)
threads = []
# Don't bother with . and ..
next if [".",".."].include? e
fullname = dir + File::Separator + e
if FileTest::directory?(fullname)
threads << Thread.new(fullname) do |fn|
delete_all(fn)
end
else
File.delete(fullname)
end
end
threads.each { |t| t.join }
Dir.delete(dir)
end
delete_all("/tmp/stuff")
它比非线程版本实际上快吗?我们发现答复不一致。它取决于你的操作系统和实际被删除的目录结构,即,它的深度,文件的大小等等。
在很多情况下线程是很有用的技术,但它们对代码和调试可能有些问题。当我们使用同步方法来达到正确的结果时,这是真实的。