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

return @que.shift

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

end

return obj

end

end

def max=(max)

@monitor.synchronize do

@max = max

@full_cond.broadcast

end

end

end

sync.rb 库以更多方式来完成线程的步。对于我们知道和关心的事情,它用一个counter实现了二相锁。在写本书时,它的唯一文档就是库本身。

 

 

6、Allowing Timeout of an Operation

在很多情况下,我们需要有允许完成一个动作的最长时间。这可避免死循环并允许在处理上有额外的控制层。这对网络环境下是个有用的特征,在其它环境下,我们可能或者不可能从远程服务器上得到响应。

timeout.rb库以基于线程的解决办法处理这个问题。Timeout方法执行写方法调用关联的块;当到达指定秒数时,它抛出TimeoutError错误,可以用rescue子句捕获它(见Listing7.6)。

Listing 7.6 A Timeout Example

require "timeout.rb"

 

 

flag = false

answer = nil

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

puts "Bye now..."

 

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线程期间存储获取的部分状态信息,然后以同样方式继续直到程序找到一个好的响应或超出它的时间。

9、实现并行迭代

假设你想在超过一个的对象上并行迭代。也就是说,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]

compose(x, y, z) do |a,b,c|

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]

# [8, nil, nil]

注意我们没有假设所有对象在迭代时有相同数量的条目。如果一个迭代器在其它迭代器之前运行完,它将生成nil值直到最长的迭代器运行完毕。

当然,可以写更通用的方法来从每个迭代器中抓取多于一个的值。(毕竟,不是所有迭代器都每次只返回一个值。)我们可以让第一个参数指定每次迭代的数量。

使用任意迭代器(而不是缺省的each)也是可行的。我们可以传递它们的名字做为字符串,并使用send来调用它们。当然这需要其它窍门来完成。

然而,我们认为这儿给出的例子对大多数情形足够了。我们将其它的变化留给你做为练习。

 

 

10、并行化的递归删除

只是出于乐趣,让我们使用第四章中的"External Data Manipulation"的例子并且并行化它。(没有,我们不是说平行地使用多个处理器。)这儿以线程形式给出递归删除例程。当我们找到的目录条目本身是个目录时,我们启动新线程来遍历那个目录并删除它的内容。

我们保存我们创建的线程的跟踪在称为threads的数组内;因为它是局部变量,每个线程都有它自己的那个数组的拷贝。它每次只可以由一个线程访问,这儿不需要对它同步访问。

注意我们也传递全文件名给线程块,以便我们不必为线程访问了一个可修改的值而烦心。线程使用fn做为同一变量的本地拷贝。

当我们遍历一个目录时,我们想在删除我们已完成工作的目录之前等待我们创建的线程。

def delete_all(dir)

threads = []

Dir.foreach(dir) do |e|

# 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")

它比非线程版本实际上快吗?我们发现答复不一致。它取决于你的操作系统和实际被删除的目录结构,即,它的深度,文件的大小等等。

 

 

三、总结

在很多情况下线程是很有用的技术,但它们对代码和调试可能有些问题。当我们使用同步方法来达到正确的结果时,这是真实的。


posted on 2007-02-13 14:46  woodfish  阅读(560)  评论(0编辑  收藏  举报

导航