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
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。
這隻是簡單的同步問題。最壞的部分會變得更難於管理,並且成為電腦學家和數學家研究的真正對象。
1、用臨界區完成簡單同步
簡單的同步形式是使用臨界區。當線程進入代碼的臨界區時,這個技術保證沒有其它線程將被運行直到第一個線程離開它的臨界區。
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內
線程管理和完成操作的結合是可能的,這會引起一個線程被調度,即使另一個線程在臨界區中。在最簡單情況下,新建立的線程將立即運行,而不管另一個線程是否在臨界區中。因此,這個技術應該只被用在最簡單的環境中。
2、對資源同步訪問 (mutex.rb)
讓我們拿一個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
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
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
無論何時,一個線程不想被鎖住時,這個特徵是非常有用的。
3、使用預定義同步的Queue類
線程庫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錯誤而不是鎖住線程。
4、使用條件變數
And he called for his fiddlers three.
"Old King Cole" (traditional folk tune)
條件變數是個真正的線程隊列。它與mutex一同使用,在同步線程時提供進階別的控制。