自動目錄
一般的程式,也就是單線程(thread)程式,是由程式開始往下執行,假設遇到計算耗時比較久或是回應逾時的情況,程式就會等待或是終止。
而多執行緒的程式,可在一次執行程式時間,同時進行多線程(multi-thread)的計算,在效率上可獲得即大的提升。
例如網路方面的程式,程式的瓶頸常常在連線方面等待目標回應,假設目標沒回應而逾時(timeout),程式會浪費時間等待直到逾時才繼續進行下去。若程式採用多執行緒方式,可以一邊等待一邊進行下去,當有回應後再接續處理。
適合多執行緒的程式
何種程式適合多執行緒,以下列出幾種特性:
1. 重覆性高,性質單純
2. 程式比較不重視順序,可將工作拆成小單元
3. 能避免共用資料存取鎖住的問題,或能允許部分資料遺失
4. 單一執行緒失敗可重新啟動或不影響最後結果
基本上多執行緒程式都有以上的特性,如果你的程式後面的計算或處理需要從前面的結果得到,那就不適合;此外,如果單一執行緒計算錯誤或失敗就會導致全部失敗而無法補救的話,就非常不適合寫成多執行緒。
此外,撰寫多執行緒的程式很燒腦,因為要思考很多邏輯的關係,也就是先後次序的關係。除錯也比較困難,所以多執行緒程式聽起來帥,但只適合進階的設計者。
使用多執行緒
perl5.8 後使用threads 這個模組,更早的版本請自行尋找 Thread.pm這模組
use threads::shared;
第二行可以不加,若你不需要使用 shared 這個模組。(如果對這部分不了解的可能參考[PERL] 18-套件及模組)
如果沒裝的話,可加裝此套件(CENTOS LINUX7)
執行緒類別的方法介紹
create() 建立一個執行緒,並開始執行指定的函式。他有一個別名new()。
yield() 讓別的執行緒有機會執行
list() 查看目前狀態
exit() 中止執行執行緒
執行緒的方法介紹
join() 關連:等待執行緒結束,並取回回傳值。
tid() 取得執行緒id。
detach() 分離:不理此執行緒,並放棄回傳值,如果此程序還沒執行完會繼續執行,直到他結束為止。
狀態檢查
is_running() 該執行緒是否正在執行?
is_joinable() 該執行緒是否已關連?
is_detached() 該執行緒是否已分離?
多執行緒的流程大致大可表示:
建立-->關連/脫離/中止-->結束
建立執行緒
使用 create() 函數來建立執行緒,以下範例使用多執行緒來產生指定範圍的整數亂數
my $thr = threads->create('create_random', (100,200)); print $thr->join(); sub create_random { my @p= @_; sleep(1); return int(rand($p[1]-$p[0])+$p[0]); }
第1行 建立執行緒,指定使用函數 "create_random",同時用清單的方式傳入此函數需要用到的參數。
第2行 等待此執行緒結束,並取得此執行緒的回傳值
第4~8行 副程式create_random:產生範圍內隨機整數亂數。
執行結果:
看起來和一般的程式沒有兩樣,其實我們也可以不用執行緒的方式叫用create_random()這個副程式,得到的結果是一樣的:
$result= &create_random(100,200); print $result
以上的範例可以知道,執行緒叫用副程式的方式和直接叫用差異並不大。
但是執行緒能做的遠遠比副程式來得多,因為直接叫用副程式的方法如果遇到副程式要執行比較久的話,程式就會停滯並等待結果。我們叫用100次副程式來比較兩者的差異:
寫法一、直接叫用100次副程式
for(1..100){
$sum+= &create_random(100,200);
}
寫法二、使用執行緒叫用100次副程式
for(1..100){
$thr = threads->create('create_random', (100,200));
$sum+= $thr->join();
}
上面的寫法二等於寫法一,等於每一次都等待程式執行完再啟動另一個執行序,並不會得到執行緒的優勢。
寫法三、使用執行緒叫用100次副程式
@thr=();
for(1..100){
$thr[$_] = threads->create('create_random', (100,200));
}
for(1..100){
$sum += $thr[$_]->join();
}
寫法三把叫用和取回值的過程分開,把多執行緒的效率發揮出來,我分別使用 Time:Elapse (Perl 計算經過的時間@新精讚)的套件來計算三者使用的時間,由於create_random副程式中有一秒的延遲,因此產生巨大的時間差異:
單次呼叫副程式: COST: 00:00:01.001000 sec 耗費約1.001秒
方法一:COST: 00:01:40.314674 sec 耗費約100.3秒
方法二:COST: 00:01:40.885978 sec 耗費約100.9秒
方法三:COST: 00:00:01.232909 sec 耗費約1.23秒 <== 時間大幅減少到只比單次呼叫的1.001秒慢一點點。
由方法三發現,使用執行緒可以讓程式平行執行,達到超高的效率,尤其是用於網路環境,不會因為其中一台伺服器連線等待而拖慢全部的時間。
每個副程式要執行1秒多,當累加起來會耗費大量的時間,所以多執行緒能用平行處理解決這個問題。
單一執行緒執行時間過久?
執行緒的確達到超高的效率,但如果單一個執行緒執行過久,結果又會如何?
結果就是,大家在等著他的結束,當使用 join() 方法時,就是在等待該執行緒結束,最後的執行時間會因為某一個執行緒太慢而延遲。
因此最好的做法就是先結束的先join,才不會造成等待,實際上我們常常無法知道哪一個執行緒能先join,這時可用 is_running()來檢查。
對於還在執行中的執行緒,如果單一執行緒的結果並不會影響整理的結果的話,可以使用 detach() 方法,放棄該執行緒。
以下範例產生100個隨機整數,並計算平均結果。
不一樣的是在副程式create_random_tardy中多了一個隨機的時間延遲,並在指定完100個執行緒後,開始進行結果的回收。
$sum=$n=0; @thr=(); for(1..100){ $thr[$_] = threads->create('create_random_tardy', (100,200)); } sleep(3); # 等待3秒讓大部分的執行緒跑完 for(1..100){ # 已經跑完的執行緒回收結果 if(!$thr[$_]->is_running()){ $sum += $thr[$_]->join(); $n++; # 未跑完的執行緒放棄結果 }else{ print $thr[$_]->tid(). ", "; $thr[$_]->detach(); } } print "Average: ". $sum/$n . "\n"; print "COST: $now sec\n"; sub create_random_tardy { my @p= @_; $x=rand(5); sleep($x); return int(rand($p[1]-$p[0])+$p[0]); }
執行結果:
COST: 00:00:03.409206 sec
說明:
第4~6行 建立100個執行緒
第7行 等待3秒讓大部分的執行緒跑完
第10行 利用 is_running() 檢查,如果已經跑完,回傳false,進行回傳結果回收。
第14~17行 正在運行中執行緒,印出目前執行緒id並放棄結果。
第22~27行 副程式create_random_tardy(),隨機產生範圍內的亂數
第25行 隨機產生5秒內的延遲。
執行緒一定要進行回收
在create一個執行緒後,千萬不能不理他,一定要進行回收,回收程式有 join/detach兩種。
就算只是單純列印資料而不回傳資料的執行緒,還是要回收處理,否則程式會報警告。
15 running and unjoined <== 程式結束,還有15個執行緒未執行完也未 join
30 finished and unjoined <== 程式結束,還有30個執行緒已執行完也未 join
1 running and detached <== 程式結束,還有1個執行緒未執行完但被 detach
取得執行中的清單
上面的方法讓每個執行緒進行判斷,如果執行完就回收結果(join),否則就脫離(detach)。
有沒有一種方法知道現在正在運行中的清單?有的,使用 list() 函數,可以帶入幾個參數
# 回傳未回收( non-joined/non-detached)的執行緒清單及數量
my @threads = threads->list();
my $thread_count = threads->list();
# 同 threads->list();
my @all= threads->list(threads::all);
# 回傳未回收( non-joined/non-detached)的執行緒但還在執行中的清單
my @running = threads->list(threads::running);
# 回傳未回收( non-joined/non-detached)的執行緒但已執行完可以join的清單
my @joinable = threads->list(threads::joinable);
以下範例執行完畢後,把未回收收但執行完的執行緒進行join(),並加總:
$sum = 0; my @joinable = threads->list(threads::joinable); foreach(@joinable){ $sum += $_->join(); }
使用list() 會列出所有的 threads物件,你無法知道哪個物件是叫用哪個副程式,全部混在一起,使用上應多注意。
結論
此篇算是大致上介紹完多執行緒的寫法,網路方面的程式很需要用多執行緒來完成,熟析後能替程式帶來極大的效能提升。
參考資料
1 perl docs https://perldoc.perl.org/threads
上一篇 [PERL] 22-日期和時間
回到目錄 01-撰寫第一隻PERL程式
下一篇 [PERL] 24-呼叫系統程式及評估