1. 程式人生 > >【Linux】執行緒總結:初識、建立、等待、終止、分離

【Linux】執行緒總結:初識、建立、等待、終止、分離

學習環境 :  Centos6.5 Linux 核心 2.6

Linux執行緒部分總結分為兩部分:(1)執行緒的使用 ,(2)執行緒的同步與互斥。

第一部分執行緒的使用主要介紹,執行緒的概念,建立執行緒,執行緒退出,以及執行緒的終止與分離。

第二部分主要介紹在多執行緒環境下,使用同步與互斥保護共享資源,有互斥鎖,條件變數,訊號量,以及讀寫鎖。

第一部分開始

初識執行緒

執行緒:也稱輕量級程序(Lightweight Process , LWP),是程式執行流的最小單元。而多執行緒就是指,在一個程序中有多個執行流,在同時執行。

為什麼需要多執行緒呢?

在客戶端,我們需要介面和使用者互動動作,此時就可以在後臺執行緒去處理互動邏輯。比如下載一個檔案利用一個執行緒,而同時還可以用一個執行緒響應使用者的其他操作。

在服務端,如http伺服器,會同時有多個請求需要處理,此時利用多執行緒可以大大提高請求的處理效率。

執行緒與程序的區別:

程序是資源分配的基本單位,而執行緒是排程的基本單元。作業系統中每一個執行的程序,都有它自己的地址空間,而同一程序中可以有多個執行緒,也就是多個執行流在同時執行。這裡的同時,如果是單核處理器,則此時並不是真正意義上的同時,由於處理器執行速度很快,給每個執行流分配了時間片,在單核處理器中微觀上還是順序執行,而在多核處理器中,就是真正意義上的並行。由於同一程序的多個執行緒共享同一地址空間,因此執行緒之間有互相共享的資源,也有彼此獨佔的資源。

執行緒之間共享的資源主要有

  • 地址空間
  • 資料段和程式碼段
  • 全域性變數
  • 檔案描述符表
  • 訊號處理方式(忽略或者有自定義動作)
  • 使用者ID和組ID
  • 當前工作目錄

每個執行緒各有一份的資源主要有

  • 執行緒ID
  • 上下文(暫存器,程式計數器,棧指標)
  • 棧空間
  • 狀態字
  • 訊號遮蔽字
  • 排程優先順序

程序與執行緒的區別歸納如下幾點:

1. 地址空間:程序間相互獨立,每個程序都有自己獨立的地址空間,同一程序的各執行緒間共享地址空間。某個程序內的執行緒在其他程序內不可見。
2. 通訊關係:程序間通訊有管道,訊息佇列,共享記憶體,訊號量。執行緒間通訊可以直接讀寫全域性變數來進行通訊。不管是程序還是執行緒,通訊時可能出現數據不一致的情況,需要用同步互斥機制來保證資料的一致性。
3. 切換和排程

:由於程序間獨佔資料段程式碼段等資訊,所以切換程序的時候,需要把程序間獨佔的資源切換去,把要執行的程序資源換進來,而執行緒是程序的子集,共享大部分資源,切換時只需要儲存上下文相關資訊就好,所以執行緒切換的開銷比程序切換的開銷小。

執行緒的三種狀態

執行緒主要有三種狀態分別是就緒、阻塞、執行。

就緒:執行緒具備執行的所有條件,邏輯上已可以執行,在等待處理機。

阻塞:指執行緒在等待某一時間的發生,如I/O操作。

執行:佔有處理器正在執行。

:關於程序與執行緒之間阻塞狀態的關係,在文末做了個實驗。

執行緒的控制

主要學習如何建立一個執行緒,執行緒有哪些終止方式,以及怎麼獲取一個執行緒的執行結果,判斷執行緒是否異常退出,執行緒生命結束時有沒有”遺言“。

這裡學習的執行緒庫函式由POSIX標準定義的,稱為POSIX thread 或者 pthread。在Linux中函式位於libpthread共享庫中,在gcc編譯或者Makefile中記得要加上 -lpthread選項,用來指定要連結的庫。函式在執行錯誤時的錯誤資訊將作為返回值返回,並不修改全域性變數errno,也就無法通過 perror() 列印錯誤資訊。

建立執行緒

#include <pthread.h>

int pthread_create(pthread_t *thread, 
                   const pthread_attr_t *attr, 
                   void *(*start_routine)(void *), 
                   void *arg);

描述:建立一個執行緒,用第一個引數執行緒識別符號,第二個引數設定執行緒屬性,第三個引數指定執行緒函式執行的起始地址(函式指標),第四個引數是執行函式的引數。

例項:下面的程式碼建立了兩個執行緒,並分別線上程中呼叫pthread_self()列印各自的執行緒ID,以及呼叫 getpid() 列印程序ID,為了對比也在建立執行緒的Main執行流中列印執行緒ID,和程序ID。

#include <stdio.h>
#include <pthread.h>    // pthread_create()
#include <unistd.h>     // sleep()
#include <sys/types.h>  // getpid()

// 列印每個執行緒的ID, 和進行ID

void * run_1(void *arg)  // 執行緒1 執行程式碼
{
    sleep(1);
    printf(" thread 1 tid is  %u,  pid is %u \n", pthread_self(), getpid()); 
}

void * run_2(void *arg)  // 執行緒2 執行程式碼
{

    sleep(1);
    printf(" thread 2 tid is  %u,  pid is %u \n", pthread_self(), getpid());
}

int main()
{
    pthread_t tid1, tid2;

    pthread_create(&tid1, NULL, run_1, NULL ); // 建立執行緒1
    pthread_create(&tid2, NULL, run_2, NULL ); // 建立執行緒2

    sleep(2);
    printf("I am main tid is  %u,  pid is %u \n", pthread_self(), getpid());
    return 0;
}

建立執行緒

從上圖執行結果以及對程式碼的分析可以得出:

1).執行緒1 和執行緒2 的程序ID一樣,可以說明同一個程序可以擁有多個執行緒,即多個執行流。

2 ) .我們發現在main 執行流中列印執行緒ID ,與建立的執行緒差異並不大,也就是說main 執行流也是一個執行緒。也可以這樣理解:在Linux中,一個程序預設有一個執行緒。單執行緒也就是單程序。

3 ).在程式碼中之所以要在main 的執行流中sleep(2),是因為執行緒執行順序與作業系統的排程演算法有關係,為了保證建立的執行緒1 和執行緒2 先執行,故在 main的列印之前加上sleep(2)。

4 ).線上程1 和執行緒2 的執行程式碼中都是一開始就sleep(1),而我們在main中建立執行緒的時候卻是先建立的執行緒1,但是列印結果卻是,執行緒2 先列印,這進一步證實了 3) 中所說,同一個程序中哪一個執行緒先執行與作業系統排程有關。

5 ). 有一點需要強調,當main 結束的時候,執行到return,或者呼叫exit(),所有執行緒也會隨之結束,下面的小程式證明這點。

#include <stdio.h>
#include <unistd.h> // sleep()
#include <pthread.h>
#include <stdlib.h> // exit()

void *run( void * arg)
{
    while(1)
    {
        printf("I am still alive ... \n");
        sleep(1);
    }
}

int main()
{
    pthread_t tid1;
    pthread_create(&tid1, NULL, run, NULL);

    sleep(2);
    printf(" The main thread ends and all threads end.\n");
    exit(0); // main thread quit

    return 0;
}

執行結果:新執行緒每隔1秒列印一次,主執行緒在2秒後exit,新執行緒也隨之結束。
主執行緒退出exit別的執行緒也會退出

終止執行緒

如果需要只終止某個執行緒而不是整個程序都終止,有三種方法。

1). 從執行緒函式return,對主執行緒不使用,在main函式中return 相當於exit。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>


// 終止執行緒 方法1

void * run(void * arg)
{
    printf("I am still alive, after a second I will quit.\n");
    sleep(1);

    return NULL;

    printf("Never output.\n");
}

int main()
{
    pthread_t tid1;
    pthread_create(&tid1, NULL, run, NULL);

    sleep(2); // 確保主執行緒最後退出 
    printf("The thread quit, I should quit.\n");
    return 0;
}

執行緒退出方式1

2 ). 一個執行緒可以呼叫pthread_cancel() 終止同一程序中的另一個執行緒。比較複雜,暫不分析。

3 ).執行緒可以呼叫 pthread_exit() 終止自己。

#include <stdio.h>
#include <unistd.h>
#include <pthread.h>

void * run(void *arg)
{
    printf("1 .. \n");
    printf("2 .. \n");
    sleep(1);
    printf("3 .. \n");
    printf("4 .. \n");
    pthread_exit(NULL);
    printf("never output .\n");
}

int main()
{
    pthread_t tid1; 
    pthread_create(&tid1, NULL, run, NULL);

    sleep(3);
    printf("thread quit, I should quit.\n");
    return 0;
}

執行緒退出方法3

等待執行緒

說說我理解的為什麼需要執行緒等待,有時候需要讓一個執行緒去執行一段程式碼,我們需要知道它是否幫我們完成了指定的要求,或者異常終止,這時候我們就需要獲取執行緒執行結果,執行緒退出可以通過返回值帶出或者通過pthread_exit()引數帶出,拿到它的“遺言”。執行緒等待也有回收資源的用處,如果一個執行緒結束執行但沒有被等待,那麼它類似於殭屍程序,佔用的資源在程序結束前都不會被回收,所以當一個執行緒執行完成後,我們應該等待回收資源。

我們可以注意到在上面的例子中,執行緒退出返回值和pthread_exit()的引數都是NULL,說明我們根本不關心執行緒的”死活“。

還有一個用處,在上面的例子中,我們都是在主執行緒中sleep()函式來防止新建立的執行緒還未結束,整個程序就結束,而現在我們可以用執行緒等待函式來達到這個目的。

#include <pthread.h>

int pthread_join(pthread_t thread, void ** retval);

描述:呼叫該函式的執行緒將掛起等待,直到id為thread的執行緒終止。thread執行緒以不同的方法終止,通過pthread_join() 得到的終止狀態是不同的,主要有如下幾種:

  • thread執行緒通過return 返回, retval 所指向的單元裡存放的是 thread執行緒函式的返回值。

  • thread執行緒是被的執行緒呼叫pthread_cancel() 異常終止掉,retval 所指向的單元存放的是常數PTHREAD_CANCELED.

  • 如果thread執行緒呼叫pthread_exit() 終止的,retval 所指向的單元存放的是傳給pthread_exit的引數。

下面來舉個栗子,拿到執行緒的”遺言“:

#include <stdio.h>
#include <pthread.h>
#include <unistd.h>

void* run_1(void *arg)
{
    printf(" I am thread 1 \n");
    return (void*)1; 
}

void* run_2(void *arg)
{
    printf(" I am thread 2 \n");

    pthread_exit((void*)2);
}


int main()
{
    pthread_t tid1, tid2;
    pthread_create(&tid1, NULL, run_1, NULL);
    pthread_create(&tid2, NULL, run_2, NULL);

    void* retval_1;
    void* retval_2;
    pthread_join(tid1, &retval_1);
    pthread_join(tid2, &retval_2);

    printf(" thread 1 retval is  %u \n", (int*)retval_1);
    printf(" thread 2 retval is  %u \n", (int*)retval_2);

    return 0;
}

執行結果,成功得到執行緒的”遺言“:

執行緒等待

注意:在上面的例子中,我們帶出的是整型值,如果需要帶出別的資料型別,則需要使用全域性變數或者malloc動態分配的空間,不能帶出執行緒執行函式內的變數,因為執行結束後,棧裡的資料變成垃圾資料。

執行緒分離

無論何時,一個執行緒是可結合(joinable )的或者是分離(detached)的。

當執行緒屬於可結合時,它能夠被其他執行緒join或者cancel回收資源。相反一個已經處於分離的執行緒是不能被join或cancel,資源會在終止時自動釋放。

建立一個執行緒,預設是可結合的,為了防止資源的洩露,我們可以顯示的呼叫pthread_join() 回收資源。對一個處於可結合的執行緒呼叫pthread_join()後,可以將執行緒置於分離狀態。不能對同一個執行緒呼叫兩個join,對一個已經分離的執行緒呼叫join會返回錯誤號。

其實在上面的例子中,已經有過通過join將一個執行緒分離,但是當在一個執行緒中通過呼叫pthread_join()來回收資源時,呼叫者就會被阻塞,如果需要回收的執行緒數目過多時,效率就大大下降。比如在一個Web 伺服器中, 主執行緒為每一個請求建立一個執行緒去響應動作,我們並不希望主執行緒也為了回收資源而被阻塞,因為可能在阻塞的同時有新的請求,我們可以再使用下面的方法,讓執行緒辦完事情後自動回收資源。

1 ). 在子執行緒中呼叫pthread_detach( pthread_self() )
2 ).在主執行緒中呼叫pthread_detach( tid )

可以將執行緒狀態設為分離。執行結束後會自動釋放所有資源。

#include <stdio.h>
#include <pthread.h>


void* run(void * arg)
{
    pthread_detach( pthread_self());
    printf("I will detach .. \n");
    return NULL;
}

int main()
{
    pthread_t tid1;
    pthread_create(&tid1, NULL, run, NULL);

    sleep(1); // 因為主執行緒不會掛起等待,為了保證子執行緒先執行完分離,讓主執行緒先等待1s
    int ret = 0;
    ret =  pthread_join(tid1, NULL);
    if( ret == 0)
    {
        printf(" join sucess. \n");
    }
    else
    {
        printf(" join failed. \n");
    }
    return 0;
}

執行結果:

執行緒分離

補文中提到的實驗

說明:之所有會有這個實驗,是因為之前一直對程序阻塞時,執行緒是什麼狀態不是很清楚,以及執行緒阻塞會對程序和別的執行緒有什麼影響。因此本次基於Linux中的輕量級執行緒做出實驗,實驗結果不一定對所有平臺都有效。

實驗環境:centos 6.5 Linux 2.6

實驗內容:

1). 阻塞一個執行緒,看其他執行緒是否也會阻塞,如果其他執行緒也阻塞,說明執行緒阻塞會導致整個程序阻塞。若其他執行緒未阻塞,則說明執行緒間狀態獨立。

#include <stdio.h>
#include <pthread.h>

void *run_1(void *arg)  // 執行緒1 每隔1s 列印一條
{
    while(1)
    {
        sleep(1);
        printf(" I am thread 1, tid is %u , runing ...\n", pthread_self());
    }
}

void *run_2(void *arg)  // 執行緒2 每隔1s 列印一條
{
    while(1)
    {
        sleep(1);
        printf(" I am thread 2, tid is %u , runing ... \n", pthread_self());
    }
}

void *run_3(void *arg)  // 執行緒3  等待輸入 ,輸入完畢後列印
{
    int input = 0;
    scanf("%d", &input);
    printf("iuput data id %d \n", input);
}

int main()   
{
    pthread_t tid1, tid2, tid3;

    pthread_create(&tid1, NULL, run_1, NULL);
    pthread_create(&tid2, NULL, run_2, NULL);
    pthread_create(&tid3, NULL, run_3, NULL);

    pthread_join(tid1, NULL);    // 主執行緒 等待其餘3個執行緒
    pthread_join(tid2, NULL);
    pthread_join(tid3, NULL);
    return 0;
}

實驗結果

這裡寫圖片描述

結果分析:

通過實驗結果可以看到,線上程3阻塞前後,執行緒1 和 2 的執行狀態沒有任何變化。可以說明 一個執行緒的阻塞並不會導致所有執行緒都阻塞。

2).不阻塞執行緒,阻塞程序,看其他執行緒是否阻塞,若其他執行緒也阻塞,則說明程序阻塞會導致所有執行緒阻塞。反之則反。

#include <stdio.h>
#include <pthread.h>


void* run()
{
    while(1)
    {
        sleep(1);
        printf("thread running ..\n");
    }
}

int main()
{
    pthread_t tid1;
    pthread_create(&tid1, NULL, run, NULL);

    int input;
    scanf("%d", &input);
    printf("input is %d \n", input);


    while(1)
    {
        sleep(1);
        printf("main thread running.. \n");
    }
    return 0;
}

實驗結果
執行緒實驗2

結果分析:

第二個實驗就有點尷尬,因為嚴格意義上來說在現在的實驗環境中讓程序阻塞,也只能讓主執行緒阻塞,而主執行緒除了結束的時候會導致整個程序都結束,和別的執行緒沒有什麼大的區別。因此實驗結果和實驗1區別並不大。

第一部分到此結束。

相關推薦

Linux執行總結初識建立等待終止分離

學習環境 :  Centos6.5 Linux 核心 2.6 Linux執行緒部分總結分為兩部分:(1)執行緒的使用 ,(2)執行緒的同步與互斥。 第一部分執行緒的使用主要介紹,執行緒的概念,建立執行緒,執行緒退出,以及執行緒的終止與分離。

Linux執行總結執行同步 -互斥鎖,條件變數,訊號量實現多生產者多消費者模型

學習環境 :  Centos6.5 Linux 核心 2.6 Linux執行緒部分總結分為兩部分:(1)執行緒的使用 ,(2)執行緒的同步與互斥。 第一部分執行緒的使用主要介紹,執行緒的概念,建立執行緒,執行緒退出,以及執行緒的終止與分離。【完

Linux執行

首先,執行緒池是什麼?顧名思義,就是把一堆開闢好的執行緒放在一個池子裡統一管理,就是一個執行緒池。   其次,為什麼要用執行緒池,難道來一個請求給它申請一個執行緒,請求處理完了釋放執行緒不行麼?也行,但是如果建立執行緒和銷燬執行緒的時間比執行緒處理請求的時間長,而且請求很多的情況下,我們的CPU資源都浪費在

Linux執行特定資料

概述 執行緒特定資料(執行緒私有資料)是儲存和查詢某個特定執行緒相關資料的一種機制,使用這種機制是因為我們希望每個執行緒可以訪問它自己單獨的資料副本,而不需要擔心與其他執行緒的訪問同步問題。 執行緒的好處就在於促進了程序中資料和屬性的共享,那麼設計執行緒私有

Linux執行安全的單例模式

單例模式:一種設計模式,一個物件只能初始化一次。分為餓漢模式和懶漢模式。 單例中懶漢和餓漢的本質區別在於以下幾點: 1、餓漢式是執行緒安全的,在類建立的同時就已經建立好一個靜態的物件供系統使用,以後不在改變。懶漢式如果在建立例項物件時不加上synchronized則會導致對物件的訪問不是執行

Linux執行安全-同步與互斥

執行緒安全:多個執行緒執行流對臨界資源的不安全爭搶操作 實現:如何讓執行緒之間安全對臨界資源進行操作就是同步與互斥 互斥:同一時間臨界資源的唯一訪問性 mutex(互斥量) ⼤部分情況,執行緒使⽤的資料都是區域性變數,變數的地址空間線上程棧空間內,這種情況,變數歸屬單

Linux執行安全以及可重入函式

可重入函式 &emsp函式被不同的控制流程呼叫,有可能在第一次呼叫還沒返回時就再次進入該函式,這稱為重入。 &emsp當程式執行到某一個函式的時候,可能因為硬體中斷或者異常而使得在使用者正在執行的程式碼暫時終端轉而進入你核心,這個時候如有一

Linux執行安全與可重入函式

【Linux學習】:在Linux的一段時間學習中,剛開始是模糊的,所以很久沒有進行部落格的整理,直到最近自己把Linux的學習從前往後回憶與聯絡清楚了,覺得是時候整理成部落格,變為自己的學習筆記了,先從執行緒安全和可重入函式整理,一方面是趁熱打鐵,另一方面是在這

Linux執行併發拷貝程式

據說大連某211高校的李教授越來越重口,不僅延續要求他所帶的每一個本科班,都要寫一份執行緒併發拷貝程式的傳統,而且還開始規定不能用Java語言寫作,導致我之前寫的《【Java】執行緒併發拷貝程式》(點選開啟連結)作廢。所有李教授旗下的學生,必須在毫無圖形介面的Linux系統

Linux執行三個經典同步問題

在瞭解了《同步與互斥的區別 》之後,我們來看看幾個經典的執行緒同步的例子。相信通過具體場景可以讓我們學會分析和解決這類執行緒同步的問題,以便以後應用在實際的專案中。 一、生產者-消費者問題 問題描述: 一組生產者程序和一組消費者程序共享一個初始為空、大

面試執行/執行總結

打個模板,有時間慢慢填空 執行緒總結 1.      什麼是執行緒,什麼是程序,執行緒和程序有什麼區別? 2.      使用執行緒的優點和缺點是什麼 3.      普通java程式中有執行緒嗎(JVM中的執行緒) 4.      執行緒的名字 5.      執行緒的

Linux執行同步與互斥的區別

同步與互斥這兩個概念經常被混淆,所以在這裡說一下它們的區別。 一、同步與互斥的區別 1. 同步 同步,又稱直接制約關係,是指多個執行緒(或程序)為了合作完成任務,必須嚴格按照規定的 某種先後次序來執行。 例如,執行緒 T2 中的語句 y 要使用執行緒

Windows執行漫談——.NET執行同步之Interlocked和ReadWrite鎖

摘要: 本系列意在記錄Windwos執行緒的相關知識點,包括執行緒基礎、執行緒排程、執行緒同步、TLS、執行緒池等。 這篇來說說靜態的Interlocked類和ReadWrite鎖 .NET中的Interlock

Java執行池ThreadPoolExecutor實現原理

引言 執行緒池:可以理解為緩衝區,由於頻繁的建立銷燬執行緒會帶來一定的成本,可以預先建立但不立即銷燬,以共享方式為別人提供服務,一來可以提供效率,再者可以控制執行緒無線擴張。合理利用執行緒池能夠帶來三個好處: 降低資源消耗。通過重複利用已建立的執行緒降低執行緒建立和銷燬造

雜談執行中斷——Interrupt

前言   以前有一個錯誤的認識,以為中斷操作都會丟擲異常,後來才發現並不是這樣,所以今天就來做一個關於中斷的總結。 如何關閉執行緒   已被棄用的Stop方法     早期,Thread類中有一個stop方法,用於強行關閉一個執行緒。但是後來發現此操作並不安全,強行關閉可能導致一致性問題。故stop方

執行池和Executor框架

  一 使用執行緒池的好處 二 Executor 框架 2.1 簡介 2.2 Executor框架結構(主要由三部分構成)  2.3 Executor框架使用說明示意圖 三 ThreadPoolExecutor詳解 3.1 Thread

Nim執行記憶體模型

在Nim的設計中,每一個執行緒都有自己一個獨立的heap,這意味著在多個執行緒之間不能引用同一個變數,帶來的好處是不會出現競態條件(race condition),壞處也很明顯,多執行緒之間無法共享變數。 讓我們來看個簡單的例子感受一下。為了引入多執行緒,我安裝了第三方庫w

Linux 常用命令總結(更新)

通過需求去總結一些命令是記憶的一個好方法,正在實踐中。 對檔案以及資料夾的操作命令: touch test.txt                                   //建立一個

.NET執行的暫停和掛起

C#中使得多執行緒暫停掛起的方法有多種,但各有不同。我逐個說明: sleep方法是個靜態方法,由thread類來呼叫。它只能暫停當前執行緒,不能暫停其他執行緒。它接受的引數指名這個執行緒需要休眠多少時間。 suspend方法是普通方法,由物件呼叫,他不但能夠終止

.NET執行同步

問題 多個執行緒同時訪問同一資源時,必然會出現衝突問題?比如一個執行緒嘗試從一個檔案中讀取資料,而另一個執行緒則嘗試在同一個檔案中修改資料。這樣子,資料可能變得不一致 解決方法 通常只讓一個執行緒徹底訪