Perl多線程(1):解釋器線程的特性

線程簡介

線程(thread)是輕量級進程,和進程同樣,都能獨立、並行運行,也由父線程建立,並由父線程所擁有,線程也有線程ID做爲線程的惟一標識符,也須要等待線程執行完畢後收集它們的退出狀態(好比使用join收屍),就像waitpid對待子進程同樣。html

線程運行在進程內部,每一個進程都至少有一個線程,即main線程,它在進程建立以後就存在。線程很是輕量級,一個進程中能夠有不少個線程,它們全都在進程內部並行地被調度、運行,就像多進程同樣。每一個線程都共享了進程的不少數據,除了線程本身所須要的數據,它們都直接使用父進程的,好比同一個線程解釋器、同一段代碼、同一段要處理的數據等,但每一個線程都有本身的調用棧(call stack)空間,用來存放某些臨時數據、某些狀態、某些返回信息等編程

因而,如今開始從多進程編程轉入到多線程編程。數組

Perl本身的線程

有些系統不原生支持線程模型(如某些Unix系統),在Perl 5.8中,Perl提供了屬於本身的線程模型:解釋器線程(interpreter thread, ithreads)。固然,Perl也依舊支持老線程。安全

  • Thread模塊提供老式線程
  • threads模塊提供Perl解釋器線程

能夠經過如下代碼來檢測操做系統是否支持老式線程、解釋器線程。多線程

#!/usr/bin/perl

BEGIN{
    use Config;
    if ($Config{usethreads}) {print "support old thread\n";}
    if ($Config{useithreads}) {print "support interpreter threads\n";}
}

或者簡單的使用perldoc來檢查這兩個模塊是否存在,通常來講安裝Perl的時候就會自動安裝它們:async

$ perldoc Thread
$ perldoc threads

threads模塊提供的是面向對象的解釋器線程,能夠直接使用new方法來建立一個線程,使用其它方法來維護線程。默認狀況下,Perl解釋器線程不會在線程之間共享數據和狀態信息(也就是說數據是線程本地的),若是想要共享,可使用threads::shared。而老式線程模塊Thread的線程默認是自動在線程間共享數據的,且於解釋器線程相互隔離,在編寫複雜程序時這可能會很複雜。函數

實際上,在Perl的解釋器線程被建立的時候,會將父線程中全部的變量都拷貝到本身的空間中使之成爲私有變量,這樣各線程之間就互相隔離了,而且自動實現了線程安全。若是想要在同進程的不一樣線程之間共享數據,須要專門使用threads::shared模塊將變量共享出去,這樣每一個線程都能訪問到這個變量。性能

解釋器線程這樣的行爲對編寫多線程來講很是的友好,可是這會影響Perl的線程性能,特別是父線程中數據量較大的時候,建立線程的成本以及內存佔用上是很是昂貴的。因此,在使用Perl解釋器線程的時候,應當儘可能在數據量還小的時候建立子線程。測試

建立線程

Perl線程在不少方面都像fork出來的進程同樣,可是在建立線程上,它更像是一個子程序。spa

建立線程的方式有兩種:create/new、async,create和new是等價的別名,這3種(其實是兩種)建立線程的方式除了語法上不一樣,在線程執行上是徹底一致的。

建立線程的標準方法是使用createnew方法(它們是等價的別名),而且給它一個子程序或子程序引用或匿名子程序,這表示建立一個新線程去運行這個子程序。

例如:

use threads;

my $thr = threads->create(\&sub1);

sub sub1 {
    print("In Child Thread\n");
}

這裏main線程建立了子線程運行sub1子程序,建立完成後,main線程繼續向下運行。

若是子程序要傳遞參數,直接在create/new的參數位上傳遞便可。

use threads;

sub threadsub {
    my $self = threads->self;
}

my $thr1 = threads->create(\&threadsub, 'arg1', 'arg2');
# 或者使用new
my $thr2 = threads->new(\&threadsub, @args);

若是使用async建立線程,那麼給async一個語句塊,就像匿名子程序同樣。

use threads;

my $thr = async {
    ... some code ...
}

這表示新建一個子線程來運行代碼塊中的代碼。

至於選擇create/new仍是選擇async來建立新線程,隨意。可是若是建立多個線程的話,使用create/new比較方便。並且,create/new也同樣能建立新線程執行匿名子程序。

my $thr1 = new threads \&threadsub, $arg1;
my $thr2 = new threads \&threadsub, $arg2;
my $thr3 = new threads \&threadsub, $arg3;

# create執行匿名子程序
my $thr = threads->create( sub {...} );

線程標識

因爲咱們可能會建立不少個線程,咱們須要區分它們。

第一種方式是經過給不一樣線程的子程序傳遞不一樣參數的方式來區分不一樣的線程

例如:

my $thr1 = threads->create(\&mysub,"first");
my $thr1 = threads->create(\&mysub,"second");
my $thr1 = threads->create(\&mysub,"third");

sub mysub {
    my $thr_num = shift @_;
    print "I am thread $thr_num\n";
    ...
}

第二種方式是獲取threads模塊中的線程對象,線程對象中包含了線程的id屬性。經過類方法threads->self()能夠獲取當前線程對象,有了線程對象,能夠經過tid()對象方法獲取這個線程對象的ID,固然還能夠直接使用類方法threads->tid()來獲取當前線程對象的ID。

my $myself = threads->self;
my $mytid = $myself->tid();

# 或
my $mytid = threads->tid();

對於已知道tid的線程,可使用類方法threads->object($tid)去獲取這個tid的線程對象。注意,object()只能獲取正激活的線程對象,對於joined和detached線程(join和detach見下文),都返回undef,不只如此,對於沒法收集的線程對象,object()都返回undef,例如收集$tid不存在的線程。

線程對象的ID是從0開始計算的,而後每新建一個子線程,ID就加1.0號線程就是每一個進程建立時的main線程,main線程再建立一個新子線程,這個新子線程的ID就是1。

能夠比較兩個線程是不是同一個線程,使用equal()方法(或者重載的==!=符號)便可,它們都基於線程ID進行比較:

print "Equal\n" if $self->equal($thr);
print "Equal\n" if $self == $thr;

線程狀態和join、detach

Perl中的線程其實是一個子程序代碼塊,它可能會有子程序的返回值,因此父線程須要接收子線程的返回值。不只如此,就像父進程須要使用wait/waitpid等待子進程併爲退出的子進程收屍同樣,父線程也須要等待子線程退出併爲子線程收屍(作最後的清理工做)。爲線程收屍是很重要的,若是隻建立了幾個運行時間短的子線程,那麼操做系統可能會自動爲子線程收屍,但建立了一大堆的子線程,操做系統可能不會給咱們什麼幫助,咱們要本身去收屍。

join()方法的功能就像waitpid同樣,當父線程中將子線程join()後,表示將子線程從父線程管理的一個線程表中加入到父線程監控的另外一個列表中(實際上並不是如此,只是修改了進程的狀態而已,稍後解釋),這個列表中的全部線程是該父線程都須要等待的。因此,將join()方法的"加入"含義看做是加入到了父線程的某個監控列表中便可

join()作三件事:

  • 等待子線程退出,等待過程當中父線程一直阻塞
  • 子線程退出後,爲子線程收屍(OS clean up)
  • 若是子線程有返回值,則收集返回值
    • 而返回值是有上下文的,根據標量(scalar)、列表(list)、空(void)上下文,應該在合理的上下文中使用返回值
    • 線程上下文相關,稍後解釋

例如:

use threads;

my ($thr) = threads->create(\&sub1);

# join,父進程等待、收屍、收集返回值
my @returnData = $thr->join();

print 'thread returned: ', join('@', @returnData), "\n";

sub sub1 {
    # 返回值是列表
    return ('fifty-six', 'foo', 2);
}

join的三件事中,若是不想要等待子線程執行完畢,可使用detach(),它將子線程脫離父線程,父線程再也不阻塞等待。由於已經脫離,父線程也將再也不爲子線程收屍(子線程在執行完畢的時候本身收屍),父線程也沒法收集子線程的返回值致使子線程的返回值被丟棄。固然,父子關係還在,只不過當父線程退出時,子線程會繼續運行,這時纔會擺脫父線程成爲孤兒線程,這就像daemon進程(本身成立進程組)和父進程同樣。

剛纔使用"父線程監控的另外一個列表"來解釋join的行爲,這是不許確的。實際上,線程有6種狀態(這些狀態稍後還會解釋):

  • detached(和joined是互斥的狀態)
  • joined(和detached是互斥的狀態)
  • finished execution(執行完但尚未返回,還沒退出),實際上是running狀態剛結束,能夠被join的階段(joinable)
  • exit
  • died
  • creation failed

當執行detach()後,線程的狀態就變成detached,當執行join()後,線程的狀態就變成joined。detached線程能夠看做是粗略地看做是脫離了父線程,它沒法join,父線程也不會對其有等待、收屍、收集返回值行爲,只有進程退出時detached線程才默默被終止(detached狀態的線程也依然是線程,是進程的內部調度單元,進程終止,線程都將終止)

例如:

use threads;

sub mysub {
    #alarm 10;
    for (1..10){
        print "I am detached thread\n";
        sleep 1;
    }
}

my $thr1 = threads->new(\&mysub)->detach();

print "main thread will exit in 2 seconds\n";
sleep 2;

上面的子線程會被detach,父線程繼續運行,在2秒後進程終止,detach後的子線程會被默默終止。

更細分一點,一個線程正常執行子程序到結束能夠劃分爲幾個過程:

  • 1.線程入口,開始執行子程序。執行子程序的階段稱爲running狀態
  • 2.子程序執行完畢,但尚未返回,這個時候是running剛結束狀態,也是前文提到的finished execution狀態
    • 若是這個線程未被detach,從這個狀態開始,這個線程能夠被join(除非是detached線程),也就是joinable狀態,父線程在這個階段再也不阻塞
  • 3.線程執行完畢
    • 若是這個線程被join,則父線程對該線程收屍並收集該線程的返回值
    • 若是這個線程被detach,則這個線程本身收屍並退出
    • 若是這個線程未join也未detach,則父線程不會收屍,而且在進程退出時報告相關消息

因此從另外一種分類角度上看,線程能夠分爲:active、joined、detached三種狀態。其中detached線程已被脫離,因此不算是active線程,joined已經表示線程的子程序已經執行完畢了,也不算是active線程,只有unjoined、undetached線程纔算是active線程,active包括running、joinable這兩個過程。

整個線程的狀態和過程能夠參考下圖:

上面一直忽略了一種狀況,線程在join以前就已經運行完畢了。例如:

my $thr1 = threads->create(\&sub1);

# 父線程睡5秒,給子線程5秒的執行時間
sleep 5;
$thr1->join();

子線程先執行完畢,可是父線程還沒對它進行join,這時子線程一直處於joinable的狀態,其實這個時候子線程基本已經失去意義了,它的返回值和相關信息都保存在線程棧(或調用棧call stack),當父線程對其進行join()的時候,天然能從線程棧中找到返回值或某些信息的棧地址從而取得相關數據,也能從如今開始對其進行收屍行爲。

實際上,解釋器線程是一個雙端鏈表結構,每一個線程節點記錄了本身的屬性,包括本身的狀態。而main線程中則包含了全部子線程的一些統計信息:

typedef struct {
    /* Structure for 'main' thread
     * Also forms the 'base' for the doubly-linked list of threads */
    ithread main_thread;
 
    /* Protects the creation and destruction of threads*/
    perl_mutex create_destruct_mutex;
 
    UV tid_counter;        # tid計數器,可知道當前已經建立了幾個線程
    IV joinable_threads;   # 可join的線程
    IV running_threads;    # 正在運行的線程
    IV detached_threads;   # detached狀態的線程
    IV total_threads;      # 總線程數
    IV default_stack_size; # 線程的默認棧空間大小
    IV page_size;
} my_pool_t;

檢查線程的狀態

使用threads->list()方法能夠列出未detach的線程,列表上下文下返回這些線程列表,標量上下文下返回數量。它有4種形式:

threads->list()  # 返回non-detach、non-joined線程
threads->list(threads::all)  # 同上
threads->list(threads::running)  # non-detached、non-joined的線程對象,即正在運行的線程
threads->list(threads::joinable)  # non-detached、non-joined但joinable的線程對象,即已完成子程序執行但未返回的線程

因此,list()只能統計未detach、未join的線程,::running返回的是正在運行子程序主體的線程,::joinable返回的是已完成子程序主體的線程,::all返回的是它們之和。

此外,咱們還能夠直接去測試線程的狀態:

$thr->is_running()
若是該線程正在運行,則返回true

$thr->is_joinable()
若是該線程已經完成了子程序的主體(即running剛結束),且未detach未join,換句話說,這個線程是joinable,因而返回true

$thr->is_detached()
threads->is_detached()
測試該線程或線程自身是否已經detach

線程的上下文環境

由於解釋器線程其實是一個運行的子程序,而父線程可能須要收集子線程的返回值(join()的行爲),而返回值在不一樣上下文中有不一樣的行爲。

仍之前面的示例來解釋:

use threads;

# my(xxx):列表上下文
# my xxx:標量上下文
my ($thr) = threads->create(\&sub1);

# join,父進程等待、收屍、收集返回值
# @arr:列表上下文
my @returnData = $thr->join();

print 'thread returned: ', join('@', @returnData), "\n";

sub sub1 {
    # 返回值是列表
    return ('fifty-six', 'foo', 2);
}

上面的建立子線程後,父線程將這個子線程join()時一直阻塞,直到子線程運行完畢,父線程將子線程的返回值收集到數組@returnData中。由於子程序的返回值是一個列表,因此這裏join的上下文是列表上下文。

其實,子線程的上下文是在被建立出來的時候決定的,這樣子程序中能夠出現wantarray()。因此,在線程被建立時、在join時上下文都要指定:前者決定線程入口(即子程序)執行時所處何種上下文,後者決定子程序返回值環境。這兩個地方的上下文不必定要同樣,例如建立線程的時候在標量上下文環境下,表示子程序在標量上下文中執行,而join的時候能夠放在空上下文表示丟棄子程序的返回值。

容許三種上下文:標量上下文、列表上下文、空上下文。

對於join時的上下文沒什麼好解釋的,根據上下文環境將返回值進行賦值而已。可是建立線程時的上下文環境須要解釋。有顯式和隱式兩種方式來指定建立線程時的上下文。

隱式上下文天然是經過所處上下文環境來暗示。

# 列表上下文建立線程
my ($thr) = threads->create(...);

# 標量上下文建立線程
my $thr = threads->create(...);

# 空上下文建立線程
threads->create(...);

顯式上下文是在create/new建立線程的時候,在第一個參數位置上指定經過一個hash引用來指定上下文環境。也有兩種方式:

# 列表上下文建立線程
my $thr = threads->create({ 'context' => 'list' }, \&sub1)
my $thr = threads->create({ 'list' => 1 }, \&sub1)

# 標量上下文建立線程
my $thr = threads->create({ 'context' => 'scalar' }, \&sub1)
my $thr = threads->create({ 'scalar' => 1 }, \&sub1)

# 空上下文建立線程
my $thr = threads->create({ 'context' => 'void' }, \&sub1)
my $thr = threads->create({ 'void' => 1 }, \&sub1)

線程的退出

正常狀況而且大多狀況下,線程都應該經過子程序return的方式退出線程。可是也有其它可能。

threads->exit()
線程自身能夠調用threads->exit()以便在任什麼時候間點退出。這會使得線程在標量上下文返回undef,在列表上下文返回空列表。若是是在main線程中調用`threads->exit()`,則等價於exit(0)

threads->exit(status)
在線程中調用時,等價於threads->exit(),退出狀態碼status會被忽略。在main線程中調用時,等價於exit(status)

die()
直接調用die函數會讓線程直接退出,若是設置了 $SIG{__DIE__} 的信號處理機制,則調用該處理方法,像通常狀況下的die同樣

exit(status)
在線程內部調用exit()函數會致使整個程序終止(進程中斷),因此不建議在線程內部調用exit()。可是能夠改變exit()終止整個程序的行爲,見下面幾個設置

use threads 'exit'=>'threads_only'
全局設置,使得在線程內部調用exit()時不會致使整個程序終止,而是隻讓線程終止。因爲這是全局設置,因此不是很建議設置。另外,該設置對main線程無效

threads->create({'exit'=>'thread_only},\&sub1)
在建立線程的時候,就設置該線程中的exit()只退出當前線程

$thr->set_thread_exit_only(bool)
修改當前線程中的exit()效果。若是給了true值,則線程內部調用exit()將只退出該線程,給false值,則終止整個程序。對main線程無效

threads->set_thread_exit_only(bool)
類方法,給true值表示當前線程中的exit()只退出當前線程。對main線程無效

最可能須要的退出方式是threads->exit()threads->exit(status),若是對於線程中嚴重錯誤的問題,則可能須要的是die或exit()來終止整個程序。

線程暫時放棄CPU

有時候可能想要讓某個線程短暫地放棄CPU轉交給其它線程,可使用yield()類方法。

threads->yield();

yield()和操做系統平臺有關,不必定真的有效,且出讓CPU的時間也必定能保證。

線程的信號處理

在threads定義的解釋器線程中,能夠在線程內部定義信號處理器(signal handler),並經過$thr->kill(SIGNAME)的方式發送信號(對於某些自動觸發的信號處理,稍後解釋),kill方法會返回線程對象以便進行鏈式調用方法。

例如,在main線程中發送SIGKILL信號,並在線程內部處理這個信號。

use threads;

sub thr_func {
    # Thread signal handler for SIGKILL
    $SIG{KILL} = sub { 
        print "Caught Signal: SIGKILL\n";
        threads->exit();
    }
    ...
}

my $thr = threads->create('thr_func');

...

# send SIGKILL to terminate thread, then detach 
# it so that it will clean up automatically
$thr->kill('KILL')->detach();

其實,threads對於線程信號的處理方式是模擬的,不是真的從操做系統發送信號(操做系統發送的信號是給進程的,會被main線程捕獲)。模擬的邏輯也很簡單,經過threads->kill()發送信號給指定線程,而後經過調用子程序中的%SIG中的signal handler便可。

例如上面的示例中,咱們想要發送KILL信號,但這個信號不是操做系統發送的,而是模擬了一個KILL信號,表示是要終止線程的執行,因而調用線程中的SIGKILL對應的signal handler,僅此而已。

可是,有些信號是某些狀況下自動觸發的,好比在線程中使用一個alarm計時器,在計時結束時它會發送SIGALRM信號給進程,這會使得整個進程都退出,而不只僅是那個單獨的線程,這顯然不是咱們所期待的結果。

實際上,操做系統所發送的信號都會在main線程中被捕獲。因此若是想要處理上面的問題,只需在main線程中定義對應操做系統發送的信號的signal handler,並在handler中從新使用threads->kill()發送這個信號給指定線程,從而間接實現"信號->線程"的機制

例如,在線程中使用alarm並在計時結束的時候中止該線程。

use threads qw(yield);

# 帶有計時器的線程
my $thr = threads->create(
    sub {
        threads->yield();
        eval {
            $SIG{ALRM} = sub {die "Timeout";};
            alarm 10;
            ... do somework ...
        };
        if ( $@ =~ /Timeout/) {
            warn "thread timeout";
        }
    }
);

$SIG{ALRM} = sub { $thr->kill('ALRM') };
... main thread continue ...

原文出處:https://www.cnblogs.com/f-ck-need-u/p/10420910.html

相關文章
相關標籤/搜索