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")
它比非線程版本實際上快嗎?我們發現回覆不一致。它取決於你的作業系統和實際被刪除的目錄結構,即,它的深度,檔案的大小等等。
三、總結
在很多情況下線程是很有用的技術,但它們對代碼和調試可能有些問題。當我們使用同步方法來達到正確的結果時,這是真實的。