1. 程式人生 > >POSIX 執行緒詳解(1)——一種支援記憶體共享的簡捷工具

POSIX 執行緒詳解(1)——一種支援記憶體共享的簡捷工具

級別: 初級

2000 年 7 月 01 日

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

瞭解如何正確運用執行緒是每一個優秀程式設計師必備的素質。執行緒類似於程序。如同程序,執行緒由核心按時間分片進行管理。在單處理器系統中,核心使用時間分片來模擬執行緒的併發執行,這種方式和程序的相同。而在多處理器系統中,如同多個程序,執行緒實際上一樣可以併發執行。

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

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

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





回頁首


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

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





回頁首


如果熟悉 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 執行緒的簡單示例程式:



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





回頁首


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() 呼叫失敗。





回頁首


無父,無子

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

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

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





回頁首


同步漫遊

現在我們來看一些程式碼,這些程式碼做了一些意想不到的事情。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);
}





回頁首


如同第一個程式,這個程式建立一個新執行緒。主執行緒和新執行緒都將全域性變數 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 做更改時通知其它執行緒“不要靠近”。我將在下一篇文章中講解如何做到這一點。到時候見。



參考資料



關於作者

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

相關推薦

POSIX 執行(1)——支援記憶體共享簡捷工具

級別: 初級 2000 年 7 月 01 日 POSIX(可移植作業系統介面)執行緒是提高程式碼響應和效能的有力手段。在本系列中,Daniel Robbins 向您精確地展示在程式設計中如何使用執行緒。其中還涉及大量幕後細節,讀完本系列文章,您完全可以運用 POSIX 執

POSIX 執行

執行緒是有趣的 瞭解如何正確運用執行緒是每一個優秀程式設計師必備的素質。執行緒類似於程序。如同程序,執行緒由核心按時間分片進行管理。在單處理器系統中,核心使用時間分片來模擬執行緒的併發執行,這種方式和程序的相同。而在多處理器系統中,如同多個程序,執行緒實際上一樣可以併發

POSIX 執行 支援記憶體共享簡捷工具

執行緒是有趣的 瞭解如何正確運用執行緒是每一個優秀程式設計師必備的素質。執行緒類似於程序。如同程序,執行緒由核心按時間分片進行管理。在單處理器系統中,核心使用時間分片來模擬執行緒的併發執行,這種方式和程序的相同。而在多處理器系統中,如同多個程序,執行緒實際上一樣可以併

執行

[多執行緒詳解(一)](http://www.neilx.com) 一、概念準備 1、程序 (1)直譯:正在進行中的程式 (2)解釋:執行一個程式時,會在記憶體中為程式開闢空間,這個空間就是一個程序。 (3)注意:一個程序中不可能沒有執行緒,只有有了執行緒才能執行; 程序只

Java執行(1)-概念與原理

一、程序與執行緒         程序是指一個記憶體中執行的應用程式,每個程序都有自己獨立的一塊記憶體空間,即程序空間或(虛空間)。程序不依賴於執行緒而獨立存在,一個程序中可以啟動多個執行緒。比如在Windows系統中,一個執行的exe就是一個程序。         執行

【Boost】boost庫中thread多執行1

1. 概述 執行緒就是,在同一程式同一時間內允許執行不同函式的離散處理佇列。 這使得一個長時間去進行某種特殊運算的函式在執行時不阻礙其他的函式變得十分重要。 執行緒實際上允許同時執行兩種函式,而這兩個函式不必相互等待。 一旦一個應用程式啟動,它僅包含一個預設執行緒。 此執行

【Boost】boost庫中thread多執行1——thread入門與簡介

1. 概述 執行緒就是,在同一程式同一時間內允許執行不同函式的離散處理佇列。 這使得一個長時間去進行某種特殊運算的函式在執行時不阻礙其他的函式變得十分重要。 執行緒實際上允許同時執行兩種函式,而這兩個函式不必相互等待。一旦一個應用程式啟動,它僅包含一個預設執行緒。 此執

POSIX 執行

那麼為什麼對於大多數合作性任務,多執行緒比多個獨立的程序更優越呢?這是因為,執行緒共享相同的記憶體空間。不同的執行緒可以存取記憶體中的同一個變數。所以,程式中的所有執行緒都可以讀或寫宣告過的全域性變數。如果曾用 fork() 編寫過重要程式碼,就會認識到這個工具的重要性。為什麼呢?雖然 fork() 允許

執行1支援記憶體共享簡捷工具

執行緒是有趣的 瞭解如何正確運用執行緒是每一個優秀程式設計師必備的素質。執行緒類似於程序。如同程序,執行緒由核心按時間分片進行管理。在單處理器系統中,核心使用時間分片來模擬執行緒的併發執行,這種方式和程序的相同。而在多處理器系統中,如同多個程序,執行緒實際上一樣可以併發執行

Android的執行(幾實現方法及區別)

一、Android執行緒的定義和特點     1.什麼是執行緒: 執行緒是一種輕量級程序,大多數情況下用於執行非同步操作。在一個Android 程式開始執行的時候,會單獨啟動一個程序,同時會產生一個UIThread執行緒(main執行緒)。一個Android 程式預設情況

c++11多執行

  原文作者:aircraft 原文連結:https://www.cnblogs.com/DOMLX/p/10914162.html              最近是恰好寫了一些c++11多執行緒有關的東西,就寫一下筆記留著以後自己忘記回來看吧,也不是專門寫給讀者看的,我就想到哪就寫到哪吧 &nbs

支援記憶體共享簡捷工具

//////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

IOS三執行

iOS的三種多執行緒技術 1.NSThread 每個NSThread物件對應一個執行緒,量級較輕(真正的多執行緒) 2.以下兩點是蘋果專門開發的“併發”技術,使得程式設計師可以不再去關心執行緒的具體使用問題 ØNSOperation/NSOperationQueu

std::thread執行1

## 目錄 - [目錄](#目錄) - [簡介](#簡介) - [執行緒的使用](#執行緒的使用) - [執行緒的建立](#執行緒的建立) - [執行緒的方法和屬性](#執行緒的方法和屬性) - [std::jthread (C++20)](#stdjthread-c20) - [sto

Java執行

這篇文章計劃講一下整個Java執行緒的生命週期。 要了解一個執行緒,首先要從它的建立說起,然後是執行緒的執行以及執行緒與執行緒之間的互動,最後是執行緒的銷燬。 一、執行緒的建立 執行緒的建立有四種方式: 1)繼承Thread類建立執行緒 2)實現Runnable介面建立執行緒 3)使用C

執行(二)

多執行緒詳解(二) 在正式介紹執行緒建立的第二種方法之前,我們接著多執行緒詳解(一),講一下:對執行緒的記憶體圖、執行緒的狀態,為下面的學習打下基礎,小夥伴們不要急喲!! 一、多執行緒執行的記憶體圖(ps.博主沒有找到合適的畫圖工具,歡迎大神們貢獻啊) class pers

面試題之——多執行

多執行緒作為Java中很重要的一個知識點,在此還是有必要總結一下的。 一.執行緒的生命週期及五種基本狀態 關於Java中執行緒的生命週期,首先看一下下面這張較為經典的圖: 上圖中基本上囊括了Java中多執行緒各重要知識點。掌握了上圖中的各知識點,Java中的多執行緒也就基本上掌握了。主

挑戰408——作業系統(6)——執行

20世紀80年代,人們提出了比程序更小,並能獨立執行的基本單位——執行緒。如果說程序的目的是為了讓多個程式彼此之間能夠併發執行,那麼引入執行緒就是為了減少併發執行所付出的時間和空間開銷。從而使得作業系統具有更好的併發性。 執行緒的作用 由於程序是一個資源的擁有者,因此在程序的建立

iOS開發多執行

在iOS開發中,多執行緒開發是非常重要的核心之一,這篇文章和大家分享一下多執行緒的進階-死鎖. iOS有三種多執行緒程式設計的技術,分別是:(一)NSThread(二)Cocoa NSOperation(三)GCD(全稱:Grand Central Dispatch) 如果你對多執行緒

Java執行(深度好文)

Java執行緒:概念與原理 一、程序與執行緒         程序是指一個記憶體中執行的應用程式,每個程序都有自己獨立的一塊記憶體空間,即程序空間或(虛空間)。程序不依賴於執行緒而獨立存在,一個程序中可以啟動多個執行緒。比如在Windows系統中,一個執行的ex