線程概述
線程是一個單一的執行流程,它是所有程式執行過程中最小的控制單位,即能被 CPU
所調度的最小任務單元。線程與進程之間既有聯絡,又完全不同。簡單地說,一個線程必然屬於某一個進程,而一個進程包含至少一個或者多個線程。早期的電腦
系統一次只能運行一個程式,因此,當有多個程式需要執行的時候,唯一的辦法就是讓它們排成隊,按順序串列執行。進程的出現打破了這種格局,CPU
資源按時間片被分割開來,分配給不同的進程使用。這樣一來,從微觀上看進程的執行雖然仍是串列的,但是從宏觀上看,不同的程式已經是在並存執行了。如果我
們把同樣的思想運用到進程上,很自然地就會把進程再細分成更小的執行單位,即線程。由於一個進程又往往需要同時執行多個類似的任務,因此這些被細分的線程
之間可以共用相同的程式碼片段,資料區段和檔案控制代碼等資源。有了進程,我們可以在一台單 CPU 電腦系統上同時運行 Firefox 和
Microsoft Office Word 等多個程式;有了線程,我們可以使 Firefox 在不同的標籤裡同時載入多個不同的頁面,在
Office Word 裡編輯文檔的同時進行語法錯誤檢查。因此,線程給我們帶來了更高的 CPU
利用率、更快速的程式響應、更經濟地資源使用方式和對多 CPU 的體繫結構更良好的適應性。
Perl 線程的曆史
5005threads 執行緒模式
Perl 對線程的支援最早可以追溯到 1998 年 7 月發布的 Perl v5.005。其發布申明指出,Perl v5.005
中加入了對作業系統級線程的支援,這個新特性是一個實驗性的產品,這也就是我們現在所稱的 5005threads 執行緒模式。對於
5005threads 執行緒模式來說,預設情況下,所有資料結構都是共用的,所以使用者必須負責這些共用資料結構的同步訪問。如今
5005threads 已經不再被推薦實用,Perl v5.10 以後的版本裡,也將不會再支援 5005threads 執行緒模式。
ithreads 執行緒模式
2000 年 5 月發布的 Perl v5.6.0 中開始引入了一個全新的執行緒模式,即 interpreter threads,
或稱為 ithreads,也正是在這個版本的發布申明中第一次提出了 5005threads
執行緒模式將來可能會被禁用的問題。儘管如此,ithreads 在那個時候還是一個新的實驗性的執行緒模式,使用者並不能直接使用它,唯一的辦法是通過
fork 函數類比。經過兩年時間的發展,到 2002 年 7 月,Perl v5.8.0 正式發布,這時 ithreads
已經是一個相對成熟的執行緒模式,發布申明中也鼓勵使用者從老的 5005threads 執行緒模式轉換到新的 ithreads 執行緒模式,並明確指出
5005threads 執行緒模式最終將被淘汰。本文後面所討論的所有內容也都是基於新的 ithreads 執行緒模式。在 ithreads
執行緒模式中,最與眾不同的特點就在於預設情況一下一切資料結構都不是共用的,這一點我們會在後面內容中有更深刻的體會。
現有環境支援哪種執行緒模式
既然 Perl 中有可能存在兩種不同的執行緒模式,我們很自然地就需要判斷現有 Perl 環境到底支援的是哪一種線程實現方式。歸納起來,我們有兩種方法:
- 在 shell 裡,我們可以通過執行 perl – V | grep usethreads 命令來擷取當前執行緒模式的相關資訊,例如
清單 1. shell 中查詢 Perl 當前執行緒模式
> perl -V | grep use.*threads config_args='-des -Doptimize=-O2 -g -pipe -m32 -march=i386 -mtune=pentium4 -Dversion=5.8.5 -Dmyhostname=localhost -Dperladmin=root@localhost -Dcc=gcc -Dcf_by=Red Hat, Inc. -Dinstallprefix=/usr -Dprefix=/usr -Darchname=i386-linux -Dvendorprefix=/usr -Dsiteprefix=/usr -Duseshrplib -Dusethreads -Duseithreads -Duselargefiles -Dd_dosuid -Dd_semctl_semun -Di_db -Ui_ndbm -Di_gdbm -Di_shadow -Di_syslog -Dman3ext=3pm -Duseperlio -Dinstallusrbinperl -Ubincompat5005 -Uversiononly -Dpager=/usr/bin/less -isr -Dinc_version_list=5.8.4 5.8.3 5.8.2 5.8.1 5.8.0' usethreads=define use5005threads=undef useithreads=define usemultiplicity=define |
從結果中不難看出,在當前的 Perl 環境中提供了對 ithreads 執行緒模式的支援。
- 在 Perl 程式中,我們也可以通過使用 Config 模組來動態擷取 Perl 執行緒模式的相關資訊,例如
清單 2. Perl 程式中動態擷取當前 Perl 執行緒模式
#!/usr/bin/perl # use Config; if( $Config{useithreads} ) { printf("Hello ithreads/n") } elsif( $Config{use5005threads} ) { printf("Hello 5005threads/n"); } else { printf("Can not support thread in your perl environment/n"); exit( 1 ); } |
值得一提的是,對於 5005threads 和 ithreads 執行緒模式,Perl 同時只能支援其中的一種。你不可能在某一個 Perl 環境中同時使用這兩種執行緒模式。本文後面討論的所有內容都是基於 ithreads 執行緒模式的。
Perl 線程的生命週期
建立線程
線程作為 Perl
中的一種實體,其一生可以粗略的分為建立,運行與退出這三個階段。建立使得線程從無到有,運行則是線程完成其主要工作的階段,退出自然就是指線程的消亡。
線程的運行和普通函數的執行非常類似,有其入口參數,一段特定的代碼流程以及執行完畢後返回的一個或一組結果,唯一與普通函數調用的不同之處就在於建立線
程的執行與當前線程的執行是並行的。
Perl 裡建立一個新的線程非常簡單,主要有兩種方法,他們分別是:
- 使用 threads 包的 create() 方法,例如
清單 3. 通過 create() 方法建立線程
use threads; sub say_hello { printf("Hello thread! @_./n"); return( rand(10) ); } my $t1 = threads->create( /&say_hello, "param1", "param2" ); my $t2 = threads->create( "say_hello", "param3", "param4" ); my $t3 = threads->create( sub { printf("Hello thread! @_/n"); return( rand(10) ); }, "param5", "param6" ); |
- 使用 async{} 塊建立線程,例如
清單 4. 通過 async{} 塊建立線程
#!/usr/bin/perl # use threads; my $t4 = async{ printf("Hello thread!/n"); }; |
join 方法和 detach 方法
線程一旦被成功建立,它就立刻開始運行了,這個時候你面臨兩種選擇,分別是 join 或者 detach 這個建立線程。當然你也可以什麼都不做,不過這可不是一個好習慣,後面我們會解釋這是為什麼。
我們先來看看 join 方法, 這也許是大多數情況下你想要的。從字面上來理解,join
就是把新建立的線程結合到當前的主線程中來,把它當成是主線程的一部分,使他們合二為一。join
會觸發兩個動作,首先,主線程會索取建立線程執行結束以後的傳回值;其次,建立線程在執行完畢並返回結果以後會自動釋放它自己所佔用的系統資源。例如
清單 5. 使用 join() 方法收割建立線程
#!/usr/bin/perl # use threads; sub func { sleep(1); return(rand(10)); } my $t1 = threads->create( /&func ); my $t2 = threads->create( /&func ); printf("do something in the main thread/n"); my $t1_res = $t1->join(); my $t2_res = $t2->join(); printf("t1_res = $t1_res/nt2_res = $t2_res/n"); |
由此我們不難發現,調用 join 的時機是一個十分有趣的問題。如果調用 join
方法太早,建立線程尚未執行完畢,自然就無法返回任何結果,那麼這個時候,主線程就不得不被阻塞,直到建立線程執行完畢之後,才能獲得傳回值,然後資源會
被釋放,join 才能結束,這在很大程度上破話了線程之間的並行性。相反,如果調用 join
方法太晚,建立線程早已執行完畢,由於一直沒有機會返回結果,它所佔用的資源就一直無法得到釋放,直到被 join
為止,這在很大程度上浪費了寶貴的系統資源。因此,join
建立線程的最好時機應該是在它剛剛執行完畢的時候,這樣既不會阻塞當前線程的執行,又可以及時釋放建立線程所佔用的系統資源。
我們再來看看 detach 方法,這也許是最省心省力的處理方法了。從字面上來理解,detach
就是把新建立的線程與當前的主線程剝離開來,讓它從此和主線程無關。當你使用 detach
方法的時候,表明主線程並不關心建立線程執行以後返回的結果,建立線程執行完畢後 Perl 會自動釋放它所佔用的資源。例如
清單 6. 使用 detach() 方法剝離線程
#!/usr/bin/perl # use threads; use Config; sub say_hello { my ( $name ) = @_; printf("Hello World! I am $name./n"); } my $t1 = threads->create( /&say_hello, "Alex" ); $t1->detach(); printf("doing something in main thread/n"); sleep(1); |
一個建立線程一旦被 detach 以後,就無法再 join 了。當你使用 detach
方法剝離線程的時候,有一點需要特別注意,那就是你需要保證被建立的線程先於主線程結束,否則你建立的線程會被迫結束,除非這種結果正是你想要的,否則這
也許會造成異常情況的出現,並增加程式調試的難度。
本節的開始我們提到,新線程被建立以後,如果既不 join,也不 detach 不是一個好習慣,這是因為除非明確地調用 detach
方法剝離線程,Perl 會認為你也許要在將來的某一個時間點調用
join,所以建立線程的傳回值會一直被儲存在記憶體中以備不時之需,它所佔用的系統資源也一直不會得到釋放。然而實際上,你打算什麼也不做,因此寶貴的系
統資源直到整個 Perl 應用結束時才被釋放。同時,由於你即沒有調用 join 有沒有調用 detach,應用結束時 Perl
還會返回給你一個線程非正常結束的警告。
線程的消亡
大多數情況下,你希望你建立的線程正常退出,這就意味著線程所對應的函數體在執行完畢後返回並釋放資源。例如在清單 5
的樣本中,建立線程被 join 以後的退出過程。可是,如果由於 detach
不當或者由於主線因某些意外的異常提前結束了,儘管它所建立的線程可能尚未執行完畢,但是他們還是會被強制中止,正所謂皮之不存,毛將焉附。這時你也許會
得到一個類似於“Perl exited with active threads”的警告。
當然,你也可以顯示地調用 exit() 方法來結束一個線程,不過值得注意的是,預設情況下,如果你在一個線程中調用了 exit()
方法, 其他線程都會隨之一起結束,在很多情況下,這也許不是你想要的,如果你希望 exit()
方法只在調用它的線程內生效,那麼你在建立該線程的時候就需要設定’ exit ’ => ’ thread_only ’。例如
清單 7. 為某個線程設定’ exit ’ => ’ thread_only ’屬性
#!/usr/bin/perl # use threads; sub say_hello { printf("Hello thread! @_./n"); sleep(10); printf("Bye/n"); } sub quick_exit { printf("I will be exit in no time/n"); exit(1); } my $t1 = threads->create( /&say_hello, "param1", "param2" ); my $t2 = threads->create( {'exit'=>'thread_only'}, /&quick_exit ); $t1->join(); $t2->join(); |
如果你希望每個線程的 exit 方法都只對自己有效,那麼在每次建立一個新線程的時候都去要顯式設定’ exit ’ => ’ thread_only ’屬性顯然有些麻煩,你也可以在引入 threads 包的時候設定這個屬性在全域範圍內有效,例如
清單 8. 設定’ exit ’ => ’ thread_only ’為全域屬性
use threads ('exit' => 'threads_only'); sub func { ... if( $condition ) { exit(1); } } my $t1 = threads->create( /&func ); my $t2 = threads->create( /&func ); $t1->join(); $t2->join(); |
共用與同步
threads::shared
和現有大多數執行緒模式不同,在 Perl ithreads
執行緒模式中,預設情況下任何資料結構都不是共用的。當一個新線程被建立以後,它就已經包含了當前所有資料結構的一份私人拷貝,建立線程中對這份拷貝的資料
結構的任何操作都不會在其他線程中有效。因此,如果需要使用任何共用的資料,都必須顯式地申明。threads::shared
包可以用來實現線程間共用資料的目的。
清單 9. 線上程中申明和使用共用資料
#!/usr/bin/perl # use threads; use threads::shared; use strict; my $var :shared = 0; # use :share tag to define my @array :shared = (); # use :share tag to define my %hash = (); share(%hash); # use share() funtion to define sub start { $var = 100; @array[0] = 200; @array[1] = 201; $hash{'1'} = 301; $hash{'2'} = 302; } sub verify { sleep(1); # make sure thread t1 execute firstly printf("var = $var/n"); # var=100 for(my $i = 0; $i < scalar(@array); $i++) { printf("array[$i] = $array[$i]/n"); # array[0]=200; array[1]=201 } foreach my $key ( sort( keys(%hash) ) ) { printf("hash{$key} = $hash{$key}/n"); # hash{1}=301; hash{2}=302 } } my $t1 = threads->create( /&start ); my $t2 = threads->create( /&verify ); $t1->join(); $t2->join(); |
鎖
多線程間既然有了共用的資料,那麼就必須對共用資料進行小心地訪問,否則,衝突在所難免。Perl ithreads 執行緒模式中內建的
lock 方法實現了線程間共用資料的鎖機制。有趣的是,並不存在一個 unlock 方法用來顯式地解鎖,鎖的生命週期以代碼塊為單位,也就是說,當
lock 操作所在的代碼塊執行結束之後,也就是鎖被隱式釋放之時。例如
清單 10. 線程中的鎖機制
use threads::shared; # in thread 1 { lock( $share ); # lock for 3 seconds sleep(3); # other threads can not lock again } # unlock implicitly now after the block # in thread 2 { lock($share); # will be blocked, as already locked by thread 1 $share++; # after thread 1 quit from the block } # unlock implicitly now after the block |
上面的樣本中,我們在 thread 1 中使用 lock 方法鎖住了一個普通的標量,這會導致 thread 2 在試圖擷取
$share 變數的鎖時被阻塞,當 thread 1 從調用 lock 的代碼塊中退出時,鎖被隱式地釋放,從而 thread 2
阻塞結束,lock 成功以後,thread 2 才可以執行 $share++ 的操作。對於數組和雜湊表來說,lock
必須用在整個資料結構上,而不是用在數組或雜湊表的某一個元素上。例如
清單 11. 在數組或雜湊表上使用鎖機制
use threads; use threads::shared; { lock(@share); # the array has been locked lock(%hash); # the hash has been locked sleep(3); # other threads can not lock again } { lock($share[1]); # error will occur lock($hash{key}); # error will occur } |
假如一個線程對某一個共用變數實施了鎖操作,在它沒有釋放鎖之前,如果另外一個線程也對這個共用變數實施鎖操作,那麼這個線程就會被阻塞,阻塞不會被自動中止而是直到前一個線程將鎖釋放為止。這樣的模式就帶來了我們常見的死結問題。例如
清單 12. 線程中的死結
use threads; use threads::shared; # in thread 1 { lock($a); # lock for 3 seconds sleep(3); # other threads can not lock again lock($b); # dead lock here } # in thread 2 { lock($b); # will be blocked, as already locked by thread 1 sleep(3); # after thread 1 quit from the block lock($a); # dead lock here } |
死結常常是多線程程式中最隱形問題,往往難以發現與調試,也增加了排查問題的難度。為了避免在程式中死結的問題,在程式中我們應該盡量避免
同時擷取多個共用變數的鎖,如果無法避免,那麼一是要盡量使用相同的順序來擷取多個共用變數的鎖,另外也要儘可能地細化上鎖的粒度,減少上鎖的時間。
訊號量
Thread::Semaphore 包為線程提供了訊號量的支援。你可以建立一個自己的訊號量,並通過 down 操作和 up
操作來實現對資源的同步訪問。實際上,down 操作和 up 操作對應的就是我們所熟知的 P 操作和 V
操作。從內部實現上看,Thread::Semaphore
本質上就是加了鎖的共用變數,無非是把這個加了鎖的共用變數封裝成了一個安全執行緒的包而已。由於訊號量不必與任何變數綁定,因此,它非常靈活,可以用來控
制你想同步的任何資料結構和程式行為。例如
清單 13. 線程中的訊號量
use threads; use threads::shared; use Thread::Semaphore; my $s = Thread::Semaphore->new(); $s->down(); # P operation ... $s->up(); # V operation |
從本質上說,訊號量是一個共用的整型變數的引用。預設情況下,它的初始值為 1,down 操作使它的值減 1,up 操作使它的值加 1。當然,你也可以自訂訊號量初始值和每次 up 或 down 操作時訊號量的變化。例如
清單 14. 線程中的訊號量
use threads; use Thread::Semaphore; my $s = Thread::Semaphore->new(5); printf("s = " . ${$s} . "/n"); # s = 5 $s->down(3); printf("s = " . ${$s} . "/n"); # s = 2 ... $s->up(4); printf("s = " . ${$s} . "/n"); # s = 6 |
線程隊列
Thread::Queue 包為線程提供了安全執行緒的隊列支援。與訊號量類似,從內部實現上看,Thread::Queue
也是把一個通過鎖機制實現同步訪問的共用隊列封裝成了一個安全執行緒的包,並提供統一的使用介面。Thread::Queue
在某些情況下可以大大簡化線程間通訊的難度和成本。例如在生產者 - 消費者模型中,生產者可以不斷地線上程隊列上做 enqueue
操作,而消費者只需要不斷地線上程隊列上做 dequeue 操作,這就很簡單地實現了生產者和消費者之間同步的問題。例如
清單 15. 生產者 - 消費者模型中對線程隊列的使用
#!/usr/bin/perl # use threads; use Thread::Queue; my $q = Thread::Queue->new(); sub produce { my $name = shift; while(1) { my $r = int(rand(100)); $q->enqueue($r); printf("$name produce $r/n"); sleep(int(rand(3))); } } sub consume { my $name = shift; while(my $r = $q->dequeue()) { printf("consume $r/n"); } } my $producer1 = threads->create(/&produce, "producer1"); my $producer2 = threads->create(/&produce, "producer2"); my $consumer1 = threads->create(/&consume, "consumer2"); $producer1->join(); $producer2->join(); $consumer1->join(); |
其他有用的非核心包
本文前面討論的所有內容都在 Perl 線程核心包的範疇之內。其實 CPAN 上還有其他一些與線程相關的非核心包,它們往往也會給 Perl 線程的使用帶來很大的便利,這裡我們選出兩個稍加介紹,拋磚引玉。
Thread::Pool 包允許你在程式中建立一批線程去完成多個類似的任務。例如當你希望建立一個多線程程式去完成檢驗 1000 個 ip 地址是否都能 ping 通的任務時,Thread::Pool 包可以給你帶來便利。
Thread::RWLock 包為線程中的讀寫操作提供了鎖機制的支援。例如當你有多個 reader 和 writer 線程共同訪問某一個或幾個檔案時,Thread::RWLock 包可以給你帶來便利。
總結
本文主要介紹了 Perl
中線程的使用方法,包括線程的建立、執行與消亡,如何線上程中使用共用變數並通過鎖機制、訊號量和線程隊列的方法來實現線程間的同步。Perl
ithreads 執行緒模式與主流執行緒模式最大的不同之處在於預設情況下任何資料結構都是非共用的,或者說 Perl 中的 ithreads
是一個“非輕量級”的執行緒模式。雖然這樣的執行緒模式增加了程式的開銷,但它並不會線上程的功能性上打折扣,同時它也使得線程間的通訊和共用變得更加簡單。
這也符合了 Perl 一貫的簡單而強大的理念和原則。
本文來自:http://www.ibm.com/developerworks/cn/linux/l-cn-perl-thread/index.html