[PERL] 23-多執行緒

URL Link //n.sfs.tw/14798

2020-11-05 14:00:37 By 張○○

一般的程式,也就是單線程(thread)程式,是由程式開始往下執行,假設遇到計算耗時比較久或是回應逾時的情況,程式就會等待或是終止。

而多執行緒的程式,可在一次執行程式時間,同時進行多線程(multi-thread)的計算,在效率上可獲得即大的提升。

例如網路方面的程式,程式的瓶頸常常在連線方面等待目標回應,假設目標沒回應而逾時(timeout),程式會浪費時間等待直到逾時才繼續進行下去。若程式採用多執行緒方式,可以一邊等待一邊進行下去,當有回應後再接續處理。

適合多執行緒的程式

何種程式適合多執行緒,以下列出幾種特性:

  1. 重覆性高,性質單純

  2. 程式比較不重視順序,可將工作拆成小單元

  3. 能避免共用資料存取鎖住的問題,或能允許部分資料遺失

  4. 單一執行緒失敗可重新啟動或不影響最後結果

基本上多執行緒程式都有以上的特性,如果你的程式後面的計算或處理需要從前面的結果得到,那就不適合;此外,如果單一執行緒計算錯誤或失敗就會導致全部失敗而無法補救的話,就非常不適合寫成多執行緒。

此外,撰寫多執行緒的程式很燒腦,因為要思考很多邏輯的關係,也就是先後次序的關係。除錯也比較困難,所以多執行緒程式聽起來帥,但只適合進階的設計者。

使用多執行緒

perl5.8 後使用threads 這個模組,更早的版本請自行尋找 Thread.pm這模組

use threads;
use threads::shared;

第二行可以不加,若你不需要使用 shared 這個模組。(如果對這部分不了解的可能參考[PERL] 18-套件及模組)

如果沒裝的話,可加裝此套件(CENTOS LINUX7)

dnf install perl-threads

執行緒類別的方法介紹

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:產生範圍內隨機整數亂數。

執行結果:

134

看起來和一般的程式沒有兩樣,其實我們也可以不用執行緒的方式叫用create_random()這個副程式,得到的結果是一樣的:

$result= &create_random(100,200);
print $result

以上的範例可以知道,執行緒叫用副程式的方式和直接叫用差異並不大。

但是執行緒能做的遠遠比副程式來得多,因為直接叫用副程式的方法如果遇到副程式要執行比較久的話,程式就會停滯並等待結果。我們叫用100次副程式來比較兩者的差異:

 

寫法一、直接叫用100次副程式

$sum=0;
for(1..100){
  $sum+= &create_random(100,200);
}

寫法二、使用執行緒叫用100次副程式

$sum=0;
for(1..100){
  $thr = threads->create('create_random', (100,200));
  $sum+= $thr->join();
}

上面的寫法二等於寫法一,等於每一次都等待程式執行完再啟動另一個執行序,並不會得到執行緒的優勢。

寫法三、使用執行緒叫用100次副程式

$sum=0;
@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]);
}

執行結果:

13, 14, 19, 21, 27, 29, 37, 48, 55, 61, 63, 67, 79, 87, 98, Average: 149.670588235294
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兩種。

就算只是單純列印資料而不回傳資料的執行緒,還是要回收處理,否則程式會報警告。

Perl exited with active threads:
    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-呼叫系統程式及評估