什麼是執行緒

先來舉一個我們生活中的例項,我們都使用過一個強大的軟體—迅雷。那你必然知道迅雷有一個邊下邊播的功能,我們在下載的時候還能同時進行觀看。這就是一個多執行緒例項。
執行緒是程序內部的執行分支。

  • 開啟迅雷軟體—–向系統核心索要資源,啟動“迅雷”程序,。
  • 開始下載一個電影—–從索要的資源中排程分配一部分資源,啟動下載執行緒。
  • 開始播放電影—–再索要的資源中排程分配一部分資源次從,啟動播放執行緒。

由上我們就不難理解,程序是資源分配的基本單位,執行緒是排程的基本單位。
執行緒有主次之分,主執行緒負責分配排程,新執行緒負責執行,每個程序只有一個主執行緒,新執行緒可以有多個。當主執行緒被殺死或者執行結束時,整個程序隨之結束,當然那麼多的執行緒也會結束。
我們可以建一個模型來理解上述文字:
這裡寫圖片描述
當主執行緒啟動程式關閉時,整個迅雷軟體就會關閉,下載,播放執行緒當然也就關閉。我們不可能在還下載完電影就關閉下載軟體吧?
所以這裡要非常注意一點,我們在進行多執行緒程式設計時,要讓主執行緒等待新執行緒執行結束後再結束。

執行緒屬性

在Linux在沒有實質意義上的執行緒,Linux是通過程序來模擬執行緒,而這些模擬出的執行緒又叫輕量級程序。我們知道,每個程序都有一個程序控制塊PCB,作業系統通過控制PCB來控制程序,程序通過PCB與地址空間,頁表的合作來訪問實體記憶體,如下圖:
這裡寫圖片描述
當我們建立多個執行緒時,就會擁有過個PCB,這些PCB(包括主執行緒)共享同一份地址空間,也就是說這些執行緒共享同一份程序資源和環境。如下圖:
這裡寫圖片描述
這些資料包括:檔案描述符表,每種訊號的處理方式,當前工作目錄,使用者ID和組ID等。
但是有些資源是每個執行緒獨有一份的:
執行緒ID,上下文,各種暫存器的值,程式計數器,棧指標,棧空間,error變數,訊號遮蔽字,排程優先順序等。尤其注意這裡的執行緒ID和棧空間。

執行緒操作函式

因為Linux上的執行緒函式位於libpthread共享庫中,因此在編譯時要加上-lpthread。例如:

gcc -o test test.c -lpthread

建立執行緒

在上文中我提到過一個名字叫執行緒ID,每個執行緒都有一個自己的執行緒ID,就像每個線程序都有一個程序ID一樣,程序ID在整個系統中是唯一的,但執行緒ID不同,執行緒ID只有在它所屬的程序上下文中才有意義。
執行緒ID是由pid_t資料型別來表示,是一個非負整數。執行緒ID是由資料型別 pthread_t來表示。
函式pthread_create用來建立一個執行緒。

int pthread_create(pthread_t *thread,const pthread_attr_t* attr,void*(*start routine)(void*),void *arg)

引數:

  • thread,為一個pthread_t型別的指標,指向一個記憶體單元。新建立的執行緒的執行緒ID會被設定在thread指向的記憶體單元。
  • attr,用於定製各種不同的執行緒屬性。
  • start routine,為一個函式指標,新建立的執行緒從start routine函式的地址開始執行。該函式的引數是一個無型別指標引數。這個引數後面我們會提到。
  • arg,為一個無型別指標,當我們需要為第三個引數函式指標傳參時,那麼便需要把這些引數放到一個結構中,然後把這個結構的地址作為arg引數傳入。可以理解為此arg引數就是上面函式指標的引數。

標頭檔案:

#include<pthread.h>

返回值:成功時返回0,失敗時返回錯誤碼。以前學的系統函式都是成功返回0,失敗返回-1,而錯誤碼儲存在全域性變數error中,pthread庫的函式都是通過返回值返回錯誤碼,雖然每個執行緒也都有一個error,但這是為了相容其他函式介面而提供的,pthread庫本身並不使用它,通過返回值返回錯誤碼更加清晰。

示例程式碼:

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

//新執行緒,每秒列印一次,共列印五次。
void* thread_run(void *arg)
{
    int count = 0;
    while(count++ < 5)
    {
        sleep(1);
        printf("thread , pid: %d , ppid: %d , tid: %lu\n",getpid(),getppid(),pthread_self());
    }
    printf("thread is over...\n");
    return  NULL;
}


int main()
{
    //主執行緒
    printf("phread\n");
    pthread_t tid;
    int ret = pthread_create(&tid,NULL,thread_run,pthread_self());
    if(ret != 0)
    {
        printf("pthread_creat error\n");
        return -1;
    }
    else
    {
            sleep(1);
            printf("main , pid: %d , ppid: %d , tid: %lu\n",getpid(),getppid(),pthread_self());
    }

    int exitCode;
    pthread_join(tid,(void**)&exitCode);//用來等待新執行緒的結束

    printf("main is over...%d\n",exitCode);
    return 0;
}

上面程式碼中用到了一個pthread_join函式,此函式的功能就是等待一個執行緒的結束。此例中讓主執行緒等待新執行緒的結束。第一個引數為新執行緒的執行緒ID,第二個引數用來接收新執行緒的退出碼。
執行結果應為主執行緒列印1次,新執行緒列印五次:

這裡寫圖片描述

執行緒終止

執行緒終止有三個方式。

  • 簡單的從啟動例程中返回,返回值是執行緒的退出碼。
    這種方式是最簡單的方式,上面例子thread_run函式直接return就是這種方式。

  • 執行緒可以被同一程序中的其他執行緒呼叫pthread_cancle終止。
    例如在主執行緒中終止新執行緒,仍為上面的例子。pthread_cancle的引數為要結束執行緒的執行緒ID。
    程式碼:

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

void* thread_run(void *arg)
{
    int count = 0;
    while(count++ < 5)
    {
        sleep(1);
        printf("thread , pid: %d , ppid: %d , tid: %lu\n",getpid(),getppid(),pthread_self());
    }
    printf("thread is over...\n");
    return  NULL;
}
int main()
{
    printf("phread\n");
    pthread_t tid;
    int ret = pthread_create(&tid,NULL,thread_run,pthread_self());
    if(ret != 0)
    {
        printf("pthread_creat error\n");
        return -1;
    }
    else
    {
            sleep(1);
            printf("main , pid: %d , ppid: %d , tid: %lu\n",getpid(),getppid(),pthread_self());
    }
    pthread_cancel(tid);//在主執行緒中結束子執行緒

    int exitCode;
    pthread_join(tid,(void**)&exitCode);

    printf("main is over...%d\n",exitCode);
    return 0;
}

執行結果應為主執行緒列印一次後直接結束,新執行緒退出碼為-1。如果一個執行緒被其他執行緒呼叫pthread_cancel異常終止掉,它返回的退出碼將是常數PTHREAD_CANCELED。這個巨集被定義在pthread.h中,值為-1。

這裡寫圖片描述

  • 執行緒可以呼叫pthread_exit自己終止自己。
    pthread_exit的引數為退出碼。注意,pthread_exit的引數或者新執行緒return的指標所指向的記憶體單元必須是全域性的或者由malloc分配的,因為當其他執行緒pthread_join得到這個返回指標時新執行緒函式已經退出了。
#include<stdio.h>
#include<pthread.h>
#include<stdlib.h>

void* thread_run(void *arg)
{
    int count = 0;
    pthread_t mid = *(pthread_t*)arg;
    while(count++ < 5)
    {
        sleep(1);
        printf("thread , pid: %d , ppid: %d , tid: %lu\n",getpid(),getppid(),pthread_self());
    }
    printf("thread is over...\n");
    pthread_exit((void*)10);
}
int main()
{
    printf("phread\n");
    pthread_t tid;
    int ret = pthread_create(&tid,NULL,thread_run,pthread_self());
    if(ret != 0)
    {
        printf("pthread_creat error\n");
        return -1;
    }
    else
    {
            sleep(1);
            printf("main , pid: %d , ppid: %d , tid: %lu\n",getpid(),getppid(),pthread_self());
    }
    int exitCode;
    pthread_join(tid,(void**)&exitCode);
    printf("main is over...%d\n",exitCode);
    return 0;
}

執行結果為:程式正常執行,但新執行緒退出碼為10。

這裡寫圖片描述

一般情況下,執行緒終止後,資源不會被立即釋放,其終止狀態一直保留到其他執行緒呼叫pthread_join獲取它的狀態為止,這時系統擦才會釋放它所佔用的資源。但是執行緒也可以被置為detach狀態,這樣的執行緒一旦終止就立刻回收它佔用額度所有資源,而不保留終止狀態,這時主執行緒也不必一直阻塞等待,可以去做其他的工作。不能對一個已經處於detach狀態的執行緒呼叫pthread_join,這樣的呼叫將返回EINVAL。對一個detach的執行緒呼叫pthread_join或者pthread_detach都可以把該執行緒置為detach狀態,也就是說,不能對同一執行緒呼叫兩次pthread_join,或者如果已經對一個執行緒呼叫了pthread_detach就不能再呼叫pthread_join了。

這裡要補充一個有關執行緒的概念:

  • 分離執行緒

    執行緒是可分離的(detached)或者結合的(joinable),一個可結合的執行緒能夠被其他執行緒收回其資源和殺死。在被其他執行緒回收之前,他的儲存器資源(eg:棧)是不可釋放的。相反一個分離的執行緒是不能被其他執行緒回收或殺死的,它的儲存器資源在它終止時由系統自動釋放。
    預設情況下,執行緒被建立成可結合的。為了避免村吃起的洩露,每個可結合線程都應該被顯示的回收,即呼叫pthread_join。若沒有被join,則會造成資源,記憶體的洩露。
    因為呼叫pthread_join後,如果應該被join的新執行緒沒有執行結束,呼叫者會被阻塞,我們有時候並不希望如此。這時可在新執行緒中加入程式碼pthread_detach(pthread_self())或者主執行緒呼叫pthread_detach(thread_id)來將新執行緒設定成可分離的,如此一來,新執行緒執行結束後會自動釋放所有資源。

話說回來。除去以上三種執行緒終止方式外,當任意一個執行緒呼叫exit或者_exit時,則整個程序的所有執行緒都終止。

我們能不能用新執行緒結束主執行緒呢?

答案是可以的,但是新執行緒如何獲取主執行緒的執行緒ID呢?
這時就用到了pthread_create函式的第四個引數。將主執行緒的執行緒ID作為第四個引數傳入。在新執行緒中就可以以引數msg獲取到。具體實現看程式碼:

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

void* thread_run(void *arg)
{
    int count = 0;
    pthread_t mid = *(pthread_t*)arg;//直接解引用msg獲取主執行緒ID
    pthread_cancel(mid);
    while(count++ < 5)
    {
        sleep(1);
        printf("thread , pid: %d , ppid: %d , tid: %lu\n",getpid(),getppid(),pthread_self());
    }
    printf("thread is over...\n");
    return  NULL;   
}
int main()
{
    printf("phread\n");
    pthread_t tid;
    pthread_t mid = pthread_self();//呼叫self函式獲取主執行緒ID
    int ret = pthread_create(&tid,NULL,thread_run,(void*)&mid);//將主執行緒ID作為第四個引數傳入
    if(ret != 0)
    {
        printf("pthread_creat error\n");
        return -1;
    }
    else
    {
            sleep(1);
            printf("main , pid: %d , ppid: %d , tid: %lu\n",getpid(),getppid(),pthread_self());
    }
    int exitCode;
    pthread_join(tid,(void**)&exitCode);

    printf("main is over...%d\n",exitCode);
    return 0;
}

執行結果為:主執行緒不執行,新執行緒執行5次後結束。

這裡寫圖片描述