天天看點

POSIX的名詞解釋 2POSIX 線程詳解

http://pauillac.inria.fr/~xleroy/linuxthreads/

http://www.ibm.com/developerworks/cn/linux/    IBM中國linux

http://www.ibm.com/developerworks/cn/          IBM中國

http://www.ibm.com/developerworks/cn/linux/thread/posix_thread1/index.html

POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
文檔選項
&lt;tr valign="top"&gt;&lt;td width="8"&gt;&lt;img alt="" height="1" width="8" src="//www.ibm.com/i/c.gif"/&gt;&lt;/td&gt;&lt;td width="16"&gt;&lt;img alt="" width="16" height="16" src="//www.ibm.com/i/c.gif"/&gt;&lt;/td&gt;&lt;td class="small" width="122"&gt;&lt;p&gt;&lt;span class="ast"&gt;未顯示需要 JavaScript 的文檔選項&lt;/span&gt;&lt;/p&gt;&lt;/td&gt;&lt;/tr&gt; <script type="text/javascript"> </script>
POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
列印本頁
<script type="text/javascript"> </script>
POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
将此頁作為電子郵件發送

級别: 初級

Daniel Robbins ([email protected]), 總裁/CEO, Gentoo Technologies, Inc.

2000 年 7 月 01 日

POSIX 線程詳解

POSIX(可移植作業系統接口)線程是提高代碼響應和性能的有力手段。在本系列中,Daniel Robbins 向您精确地展示在程式設計中如何使用線程。其中還涉及大量幕後細節,讀完本系列文章,您完全可以運用 POSIX 線程建立多線程程式。

線程是有趣的

了解如何正确運用線程是每一個優秀程式員必備的素質。線程類似于程序。如同程序,線程由核心按時間分片進行管理。在單處理器系統中,核心使用時間分片來模拟線程的并發執行,這種方式和程序的相同。而在多處理器系統中,如同多個程序,線程實際上一樣可以并發執行。

那麼為什麼對于大多數合作性任務,多線程比多個獨立的程序更優越呢?這是因為,線程共享相同的記憶體空間。不同的線程可以存取記憶體中的同一個變量。是以,程式中的所有線程都可以讀或寫聲明過的全局變量。如果曾用 fork() 編寫過重要代碼,就會認識到這個工具的重要性。為什麼呢?雖然 fork() 允許建立多個程序,但它還會帶來以下通信問題: 如何讓多個程序互相通信,這裡每個程序都有各自獨立的記憶體空間。對這個問題沒有一個簡單的答案。雖然有許多不同種類的本地 IPC (程序間通信),但它們都遇到兩個重要障礙:

  • 強加了某種形式的額外核心開銷,進而降低性能。
  • 對于大多數情形,IPC 不是對于代碼的“自然”擴充。通常極大地增加了程式的複雜性。

雙重壞事: 開銷和複雜性都非好事。如果曾經為了支援 IPC 而對程式大動幹戈過,那麼您就會真正欣賞線程提供的簡單共享記憶體機制。由于所有的線程都駐留在同一記憶體空間,POSIX 線程無需進行開銷大而複雜的長距離調用。隻要利用簡單的同步機制,程式中所有的線程都可以讀取和修改已有的資料結構。而無需将資料經由檔案描述符轉儲或擠入緊窄的共享記憶體空間。僅此一個原因,就足以讓您考慮應該采用單程序/多線程模式而非多程序/單線程模式。

POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
回頁首

線程是快捷的

不僅如此。線程同樣還是非常快捷的。與标準 fork() 相比,線程帶來的開銷很小。核心無需單獨複制程序的記憶體空間或檔案描述符等等。這就節省了大量的 CPU 時間,使得線程建立比新程序建立快上十到一百倍。因為這一點,可以大量使用線程而無需太過于擔心帶來的 CPU 或記憶體不足。使用 fork() 時導緻的大量 CPU 占用也不複存在。這表示隻要在程式中有意義,通常就可以建立線程。

當然,和程序一樣,線程将利用多 CPU。如果軟體是針對多處理器系統設計的,這就真的是一大特性(如果軟體是開放源碼,則最終可能在不少平台上運作)。特定類型線程程式(尤其是 CPU 密集型程式)的性能将随系統中處理器的數目幾乎線性地提高。如果正在編寫 CPU 非常密集型的程式,則絕對想設法在代碼中使用多線程。一旦掌握了線程編碼,無需使用繁瑣的 IPC 和其它複雜的通信機制,就能夠以全新和創造性的方法解決編碼難題。所有這些特性配合在一起使得多線程程式設計更有趣、快速和靈活。

POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
回頁首

線程是可移植的

如果熟悉 Linux 程式設計,就有可能知道 __clone() 系統調用。__clone() 類似于 fork(),同時也有許多線程的特性。例如,使用 __clone(),新的子程序可以有選擇地共享父程序的執行環境(記憶體空間,檔案描述符等)。這是好的一面。但 __clone() 也有不足之處。正如__clone() 線上幫助指出:

“__clone 調用是特定于 Linux 平台的,不适用于實作可移植的程式。欲編寫線程化應用程式(多線程控制同一記憶體空間),最好使用實作 POSIX 1003.1c 線程 API 的庫,例如 Linux-Threads 庫。參閱 pthread_create(3thr)。”

雖然 __clone() 有線程的許多特性,但它是不可移植的。當然這并不意味着代碼中不能使用它。但在軟體中考慮使用 __clone() 時應當權衡這一事實。值得慶幸的是,正如 __clone() 線上幫助指出,有一種更好的替代方案:POSIX 線程。如果想編寫 可移植的 多線程代碼,代碼可運作于 Solaris、FreeBSD、Linux 和其它平台,POSIX 線程是一種當然之選。

POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
回頁首

第一個線程

下面是一個 POSIX 線程的簡單示例程式:

thread1.c

#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
 void *thread_function(void *arg) {
  int i;
  for ( i=0; i<20; i++) {
    printf("Thread says hi!/n");
    sleep(1);
  }
  return NULL;
}
int main(void) {
  pthread_t mythread;
  
  if ( pthread_create( &mythread, NULL, thread_function, NULL) ) {
    printf("error creating thread.");
    abort();
  }
  if ( pthread_join ( mythread, NULL ) ) {
    printf("error joining thread.");
    abort();
  }
  exit(0);
}
      

要編譯這個程式,隻需先将程式存為 thread1.c,然後輸入:

$ gcc thread1.c -o thread1 -lpthread
      

運作則輸入:

$ ./thread1
      
POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
回頁首

了解 thread1.c

thread1.c 是一個非常簡單的線程程式。雖然它沒有實作什麼有用的功能,但可以幫助了解線程的運作機制。下面,我們一步一步地了解這個程式是幹什麼的。main() 中聲明了變量 mythread,類型是 pthread_t。pthread_t 類型在 pthread.h 中定義,通常稱為“線程 id”(縮寫為 "tid")。可以認為它是一種線程句柄。

mythread 聲明後(記住 mythread 隻是一個 "tid",或是将要建立的線程的句柄),調用 pthread_create 函數建立一個真實活動的線程。不要因為 pthread_create() 在 "if" 語句内而受其迷惑。由于 pthread_create() 執行成功時傳回零而失敗時則傳回非零值,将 pthread_create() 函數調用放在 if() 語句中隻是為了友善地檢測失敗的調用。讓我們檢視一下 pthread_create 參數。第一個參數 &mythread 是指向 mythread 的指針。第二個參數目前為 NULL,可用來定義線程的某些屬性。由于預設的線程屬性是适用的,隻需将該參數設為 NULL。

第三個參數是新線程啟動時調用的函數名。本例中,函數名為 thread_function()。當 thread_function() 傳回時,新線程将終止。本例中,線程函數沒有實作大的功能。它僅将 "Thread says hi!" 輸出 20 次然後退出。注意 thread_function() 接受 void * 作為參數,同時傳回值的類型也是 void *。這表明可以用 void * 向新線程傳遞任意類型的資料,新線程完成時也可傳回任意類型的資料。那如何向線程傳遞一個任意參數?很簡單。隻要利用 pthread_create() 中的第四個參數。本例中,因為沒有必要将任何資料傳給微不足道的 thread_function(),是以将第四個參數設為 NULL。

您也許已推測到,在 pthread_create() 成功傳回之後,程式将包含兩個線程。等一等, 兩個 線程?我們不是隻建立了一個線程嗎?不錯,我們隻建立了一個程序。但是主程式同樣也是一個線程。可以這樣了解:如果編寫的程式根本沒有使用 POSIX 線程,則該程式是單線程的(這個單線程稱為“主”線程)。建立一個新線程之後程式總共就有兩個線程了。

我想此時您至少有兩個重要問題。第一個問題,新線程建立之後主線程如何運作。答案,主線程按順序繼續執行下一行程式(本例中執行 "if (pthread_join(...))")。第二個問題,新線程結束時如何處理。答案,新線程先停止,然後作為其清理過程的一部分,等待與另一個線程合并或“連接配接”。

現在,來看一下 pthread_join()。正如 pthread_create() 将一個線程拆分為兩個, pthread_join() 将兩個線程合并為一個線程。pthread_join() 的第一個參數是 tid mythread。第二個參數是指向 void 指針的指針。如果 void 指針不為 NULL,pthread_join 将線程的 void * 傳回值放置在指定的位置上。由于我們不必理會 thread_function() 的傳回值,是以将其設為 NULL.

您會注意到 thread_function() 花了 20 秒才完成。在 thread_function() 結束很久之前,主線程就已經調用了 pthread_join()。如果發生這種情況,主線程将中斷(轉向睡眠)然後等待 thread_function() 完成。當 thread_function() 完成後, pthread_join() 将傳回。這時程式又隻有一個主線程。當程式退出時,所有新線程已經使用 pthread_join() 合并了。這就是應該如何處理在程式中建立的每個新線程的過程。如果沒有合并一個新線程,則它仍然對系統的最大線程數限制不利。這意味着如果未對線程做正确的清理,最終會導緻 pthread_create() 調用失敗。

POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
回頁首

無父,無子

如果使用過 fork() 系統調用,可能熟悉父程序和子程序的概念。當用 fork() 建立另一個新程序時,新程序是子程序,原始程序是父程序。這建立了可能非常有用的層次關系,尤其是等待子程序終止時。例如,waitpid() 函數讓目前程序等待所有子程序終止。waitpid() 用來在父程序中實作簡單的清理過程。

而 POSIX 線程就更有意思。您可能已經注意到我一直有意避免使用“父線程”和“子線程”的說法。這是因為 POSIX 線程中不存在這種層次關系。雖然主線程可以建立一個新線程,新線程可以建立另一個新線程,POSIX 線程标準将它們視為等同的層次。是以等待子線程退出的概念在這裡沒有意義。POSIX 線程标準不記錄任何“家族”資訊。缺少家族資訊有一個主要含意:如果要等待一個線程終止,就必須将線程的 tid 傳遞給 pthread_join()。線程庫無法為您斷定 tid。

對大多數開發者來說這不是個好消息,因為這會使有多個線程的程式複雜化。不過不要為此擔憂。POSIX 線程标準提供了有效地管理多個線程所需要的所有工具。實際上,沒有父/子關系這一事實卻為在程式中使用線程開辟了更創造性的方法。例如,如果有一個線程稱為線程 1,線程 1 建立了稱為線程 2 的線程,則線程 1 自己沒有必要調用 pthread_join() 來合并線程 2,程式中其它任一線程都可以做到。當編寫大量使用線程的代碼時,這就可能允許發生有趣的事情。例如,可以建立一個包含所有已停止線程的全局“死線程清單”,然後讓一個專門的清理線程專等停止的線程加到清單中。這個清理線程調用 pthread_join() 将剛停止的線程與自己合并。現在,僅用一個線程就巧妙和有效地處理了全部清理。

POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
回頁首

同步漫遊

現在我們來看一些代碼,這些代碼做了一些意想不到的事情。thread2.c 的代碼如下:

thread2.c

#include <pthread.h>
#include <stdlib.h>
#include <unistd.h>
#include <stdio.h>
int myglobal;
 void *thread_function(void *arg) {
  int i,j;
  for ( i=0; i<20; i++) {
    j=myglobal;
    j=j+1;
    printf(".");
    fflush(stdout);
    sleep(1);
    myglobal=j;
  }
  return NULL;
}
int main(void) {
  pthread_t mythread;
  int i;
  if ( pthread_create( &mythread, NULL, thread_function, NULL) ) {
    printf("error creating thread.");
    abort();
  }
  for ( i=0; i<20; i++) {
    myglobal=myglobal+1;
    printf("o");
    fflush(stdout);
    sleep(1);
  }
  if ( pthread_join ( mythread, NULL ) ) {
    printf("error joining thread.");
    abort();
  }
  printf("/nmyglobal equals %d/n",myglobal);
  exit(0);
}
      
POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
回頁首

了解 thread2.c

如同第一個程式,這個程式建立一個新線程。主線程和新線程都将全局變量 myglobal 加一 20 次。但是程式本身産生了某些意想不到的結果。編譯代碼請輸入:

$ gcc thread2.c -o thread2 -lpthread
      

運作請輸入:

$ ./thread2
      

輸出:

$ ./thread2
..o.o.o.o.oo.o.o.o.o.o.o.o.o.o..o.o.o.o.o
myglobal equals 21
      

非常意外吧!因為 myglobal 從零開始,主線程和新線程各自對其進行了 20 次加一, 程式結束時 myglobal 值應當等于 40。由于 myglobal 輸出結果為 21,這其中肯定有問題。但是究竟是什麼呢?

放棄嗎?好,讓我來解釋是怎麼一回事。首先檢視函數 thread_function()。注意如何将 myglobal 複制到局部變量 "j" 了嗎? 接着将 j 加一, 再睡眠一秒,然後到這時才将新的 j 值複制到 myglobal?這就是關鍵所在。設想一下,如果主線程就在新線程将 myglobal 值複制給 j 後 立即将 myglobal 加一,會發生什麼?當 thread_function() 将 j 的值寫回 myglobal 時,就覆寫了主線程所做的修改。

當編寫線程程式時,應避免産生這種無用的副作用,否則隻會浪費時間(當然,除了編寫關于 POSIX 線程的文章時有用)。那麼,如何才能排除這種問題呢?

由于是将 myglobal 複制給 j 并且等了一秒之後才寫回時産生問題,可以嘗試避免使用臨時局部變量并直接将 myglobal 加一。雖然這種解決方案對這個特定例子适用,但它還是不正确。如果我們對 myglobal 進行相對複雜的數學運算,而不是簡單的加一,這種方法就會失效。但是為什麼呢?

要了解這個問題,必須記住線程是并發運作的。即使在單處理器系統上運作(核心利用時間分片模拟多任務)也是可以的,從程式員的角度,想像兩個線程是同時執行的。thread2.c 出現問題是因為 thread_function() 依賴以下論據:在 myglobal 加一之前的大約一秒鐘期間不會修改 myglobal。需要有些途徑讓一個線程在對 myglobal 做更改時通知其它線程“不要靠近”。我将在下一篇文章中講解如何做到這一點。到時候見。

參考資料

  • 參閱 Linux threads中的文檔,Sean Walton, KB7rfa
  • 在 An Introduction to Pthreads-Tcl 中,檢視對 Tcl 的更改以使其能夠使用 POSIX 線程
  • 使用友好的 Linux pthread 線上幫助 ("man -k pthread")
  • 參考 POSIX and DCE threads for Linux首頁
  • 檢視 The LinuxThreads Library
  • Proolix ,一種簡單遵從 POSIX 标準的作業系統,用于 i8086+,一直在開發中
  • 閱讀 David R. Butenhof 的著作 Programming with POSIX Threads,書中讨論了許多問題,其中談到不使用互斥對象是可能出現的種種情況

關于作者

POSIX的名詞解釋 2POSIX 線程詳解
POSIX的名詞解釋 2POSIX 線程詳解
Daniel Robbins 居住在新墨西哥州的 Albuquerque。他是 Gentoo Technologies, Inc. 的總裁兼 CEO, Gentoo 項目的總設計師,多本 MacMillan 出版書籍的作者,包括: Caldera OpenLinux Unleashed、 SuSE Linux Unleashed和 Samba Unleashed 。Daniel 自國小二年級起就與計算機結下不解之緣,那時他首先接觸的是 Logo 程式語言,并沉溺于 Pac-Man 遊戲中。這也許就是他至今仍擔任 SONY Electronic Publishing/Psygnosis 的首席圖形設計師的原因所在。Daniel 喜歡與妻子 Mary 和剛出生的女兒 Hadassah 一起共渡時光。可通過 [email protected] 與 Daniel Robbins 取得聯系。

繼續閱讀