1. 程式人生 > >linux:執行緒的建立、執行緒等待、執行緒終止、執行緒分離

linux:執行緒的建立、執行緒等待、執行緒終止、執行緒分離

1. 什麼是執行緒

  執行緒是程序執行內部的一個執行分支,在一個程序內部執行的多種執行流;內部本質上是多個執行緒在同一個地址空間執行;第一個pcb稱之為主執行緒;有多個執行緒就有多個執行流;一個程序至少有一個執行緒

2. 圖解執行緒

  1. PCB2所代表的程序通過vfork建立一個子程序,子程序再vfork一個新的子程序,以此類推產生兩個新的子程序;

  2. 此時PCB1、PCB2、PCB3都指向同一塊虛擬地址空間,通過操作把PCB1所指向的虛擬空間的資源(主要是資料和程式碼),分成3部分分別給PCB1、PCB2、PCB3

  3. 這些程序就被稱為執行緒,這些執行緒之間滿足互不干擾的條件,且這些執行緒共用同一虛擬空間,並且共用部分資源,在訪問這些公共資源時,這些執行緒可以互相訪問到對方的資源

這裡寫圖片描述

3. linux下的執行緒

  linux下並沒有真正意義上的執行緒存在,linux中使用程序來模擬實現執行緒,父程序建立子程序,子程序執行父程序的一部分程式碼,並且與父程序共享同一個地址空間。這些一個一個被創建出來的子程序可看到為執行緒,這種執行緒也稱之為輕量級程序

注:輕量級程序(LWP)是一種實現多工的方法。與普通程序相比,LWP與其他程序共享所有(或大部分)它的邏輯地址空間和系統資源;linux下,CPU看到的所有程序都可以看作為輕量級程序

4. 執行緒的資源(私有和共享)

共享 私有
資料段,程式碼段 每個執行緒都有自己的棧結構
檔案描述符表 上下文,(包含各種計數器的值、程式計數器和棧指標)
每種訊號的處理方式 執行緒id
當前工作的目錄 errno變數(當執行緒異常退出時的錯誤退出碼)
使用者id和執行緒組id 訊號遮蔽字
排程優先順序

5. 執行緒的優缺點

優點

  1. 執行緒佔用的資源比程序少,只虛複製PCB即可
  2. 建立時代價較小
  3. 執行緒間的切換(排程)需要作業系統做的工作少很多
  4. 執行緒間共享資料更容易
  5. 在等待慢速 I/O操作結束的同時,程式可執行其他的計算任務。
  6. 計算密集型應用,為了能在多處理器系統上執行,將計算分解到多個執行緒中實現。
  7. I/O密集型應用,為了提高效能,將I/O操作重疊。執行緒可以同時等待不同的I/O操作。

缺點

  1. 效能損失(一個很少被外部事件阻塞的計算密集型執行緒往往無法與其他執行緒共享一個處理器。如果計算密集型執行緒的數量比可用的處理器多,那麼就有可能造成較大的效能損失,這裡的效能損失指的是作業系統增加了額外的同步和排程開銷,而可用的資源不變);

  2. 健壯性降低(由於執行緒是共享同一塊虛擬地址空間的,在執行期間,因時間分配上的細微偏差或者因共享了不該共享的變數而造成不良影響的可能性是很大的,也就是說執行緒之間是缺乏保護的);

  3. 缺乏訪問控制(當線上程中呼叫某些函式(OS函式,處理signal函式,呼叫kill/exit函式),可能會導致執行緒退出,從而使所有的執行緒都異常退出);

  4. 程式設計難度提高(執行緒共用同一塊虛擬地址空間,勢必在處理多執行緒時會有訪問同一個資源等問題,此時就涉及到共享資源的處理)

6. 執行緒控制

6.0 寫在前面

  在作業系統內部,它不管什麼程序執行緒的,它只以PCB為準,只有在使用者態裡才有執行緒的概念。一般實現執行緒會用到一個POSIX執行緒庫,在這裡可以通過呼叫POSIX庫裡的函式來實現有關執行緒的各種操作。不過核心中也有一種核心級執行緒。

  兩個基本型別:

  1. 使用者級執行緒:管理過程全部由使用者程式完成,作業系統核心心只對程序進行管理。如POSIX執行緒庫。

  2. 系統級執行緒(核心級執行緒):由作業系統核心進行管理。作業系統核心給應用程式提供相應的系統呼叫和應用程式介面API,以使使用者程式可以建立、執行、撤消執行緒。

POSIX執行緒庫

  由系統庫支援。執行緒的建立和撤銷以及執行緒狀態的變化都由庫函式控制並在目態(user態)完成,與執行緒相關的控制結構TCB儲存在目態並由系統維護。由於執行緒對操作不可見(作業系統可見的必然儲存在kernel態由系統維護),系統排程仍以程序為單位(同一程序內執行緒相互競爭),核心棧的個數與程序個數相對性。

  • 與執行緒有關的函式構成了一個完整的系列,絕大多數函式的名字都是以“pthread_”打頭的

  • 要使用這些庫函式,就要引入標頭檔案

  • gcc在連結這些執行緒函式庫時要使用編譯器命令的“-lpthread”選項(pthread是共享庫檔案)

6.1 建立執行緒

注:創建出新執行緒後,新執行緒去執行函式,主執行緒繼續往下執行,誰先誰後不一定,同理fork父子程序

        #include <pthread.h>

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

   //引數:
   //thread:執行緒id,將執行緒id存入,執行緒識別符號,執行緒棧的起始地址,輸出型引數
   //attr:執行緒屬性,NULL,8種選項
   //函式指標:新執行緒執行該函式指標指向的程式碼,執行緒回撥函式
   //arg:傳遞給函式指標指向的函式的引數,執行緒回撥函式的引數

    //返回值:成功返回0,失敗返回錯誤碼,如:
    //EAGAIN   描述: 超出了系統限制,如建立的執行緒太多。 
    //EINVAL   描述: tattr 的值無效。

例:

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

void* thread_func(void* arg)
{
    (void)arg;
    while(1)
    {
        printf("I am new thread\n");
        sleep(1);
    }
}

int main()
{
    pthread_t tid;

   if ( pthread_create(&tid,NULL,thread_func,NULL) == -1){
       perror("pthread_create");
       exit(1);
   }

   while(1){
       printf("I am  main thread\n");
       sleep(1);
   }

    return 0;
}

結果:

這裡寫圖片描述

6.2 執行緒ID

  執行緒是程序的一個執行分支,並且執行緒在核心中的存在狀態是輕量級程序,因此執行緒ID和程序ID存在一定的關係,可檢視:linux下的執行緒ID和程序ID

6.3 執行緒的檢視以及多執行緒的除錯

  執行緒是程序的執行分支,多個執行緒共享同一塊虛擬地址空間,在這其中,多個執行緒共享資料段、 程式碼段等等,但還是存在自己私有的一些結構,對於這些結構,我們可以通過不同的方法可以進行檢視。可參考:執行緒的檢視以及多執行緒的除錯

6.4 等待執行緒——pthread_join
  1. 功能:以阻塞的方式回收新執行緒,可以得到執行緒的退出碼,並回收其資源
  2. 如果不使用pthread_join回收執行緒,有可能造成和殭屍程序一樣的問題,造成記憶體洩漏;
    #include <pthread.h>
    int pthread_join(pthread_t thread, //要等待的執行緒ID
    void **retval);//用於接收執行緒退出的退出碼

等待執行緒的目的:

  1. 保證執行緒的退出順序:保證一個執行緒退出並且回收資源後允許下一個程序退出
  2. 回收執行緒退出時的資源情況:保證當前執行緒退出後,建立的新執行緒不會複用剛才退出執行緒的地址空間
  3. 獲得新執行緒退出時的結果是否正確的退出返回值,這個有點類似回收殭屍程序的wait,保證不會發生記憶體洩露等問題
6.5 終止執行緒

 方式1:在一個執行緒中return

  如果執行緒通過return返回,那麼retval所指向的單元裡存放的是tread函式的返回值

例:main函式建立一個新執行緒,新執行緒執行完自己的函式,使用return退出,那麼返回值就是退出碼

方式2:exit

  如果執行緒是自己呼叫exit終止的,那麼就是直接退出,並且exit表示程序的退出

exit和return的區別:

  1. return是函式的退出,exit是程序的退出

  2. return執行結束後會呼叫exit或和exit類似的函式,return會釋放區域性變數並且彈出棧楨,回到上一個函式繼續執行

方式3: 使用pthread_exit()

  執行緒自己呼叫函式終止,pthread_ jion()函式裡的retval(退出碼)就是pthread_exit的引數

#include <pthread.h>
       void pthread_exit(void *retval);
執行緒的退出函式,retval是退出碼,該函式只是當前執行緒退出,不影響其他執行緒

方式4:

  如果執行緒是通過pthread_ cancel被別的執行緒異常終止,則retval(退出碼)就是PTHREAD_ CANCELED

#include <pthread.h>
       int pthread_cancel(pthread_t thread);//執行緒ID
//可以終止自己的執行緒也可以終止別人的執行緒
//成功返回0,失敗返回-1
//終止自己算是成功退出,發回0,終止別的程序,則那個程序算是失敗退出,返回-1;
//不是立馬執行,會延遲一會,等到cancel的時間點
//系統呼叫的都是cancel點

例1:使用return退出


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

void* thread_func(void* arg)
{
    (void)arg;
    int count = 0;
    while(1)
    {
        ++count;
        if(count > 10)
            return NULL;//使用return方式退出
        printf("I am new thread:%d,count: %d\n",pthread_self(),count);
        sleep(1);
    }
}

int main()
{
    pthread_t tid;

   if ( pthread_create(&tid,NULL,thread_func,NULL) == -1){
       perror("pthread_create");
       exit(1);
   }

   while(1){
       printf("I am  main thread:%d\n",pthread_self());
       sleep(1);
   }

    return 0;
}

結果:

這裡寫圖片描述

例2:exit退出


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

void* thread_func(void* arg)
{
    (void)arg;
    int count = 0;
    while(1)
    {
        ++count;
        if(count > 10)
            exit(1);//使用return方式退出
        printf("I am new thread:%d,count: %d\n",pthread_self(),count);
        sleep(1);
    }
}

int main()
{
    pthread_t tid;

   if ( pthread_create(&tid,NULL,thread_func,NULL) == -1){
       perror("pthread_create");
       exit(1);
   }

   while(1){
       printf("I am  main thread:%d\n",pthread_self());
       sleep(1);
   }

    return 0;
}

結果:

這裡寫圖片描述

例3:pthread_exit退出

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

void* thread_func(void* arg)
{
    (void)arg;
    int count = 0;
    while(1)
    {
        ++count;
        if(count > 10)
            pthread_exit(NULL);
        printf("I am new thread:%d,count: %d\n",pthread_self(),count);
        sleep(1);
    }
}

int main()
{
    pthread_t tid;

   if ( pthread_create(&tid,NULL,thread_func,NULL) == -1){
       perror("pthread_create");
       exit(1);
   }

   while(1){
       printf("I am  main thread:%d\n",pthread_self());
       sleep(1);
   }

    return 0;
}

結果:

這裡寫圖片描述

例4:pthread_cancel

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

void* thread_func(void* arg)
{
    (void)arg;
    while(1)
    {
        printf("I am new thread:%d\n",pthread_self());
        sleep(1);
    }
}

int main()
{
    pthread_t tid;

   if ( pthread_create(&tid,NULL,thread_func,NULL) == -1){
       perror("pthread_create");
       exit(1);
   }
    int count = 0;
    void* ret;
   while(1){
       ++count;

       if(count > 5){
           pthread_cancel(tid);
           pthread_join(tid,&ret);
       }
       if(ret == PTHREAD_CANCELED)
            printf("thread is return,thread id: %d, return code:PTHREAD_CANCELED\n",tid);
        else
            printf("thread is return,thread id: %d, return code:NULL\n",tid);


       printf("I am main thread: %d,count: %d\n",pthread_self(),count);
       sleep(1);
   }

    return 0;
}

結果:

這裡寫圖片描述

6.6 執行緒分離

   6.6.1 執行緒的兩種狀態——可結合、可分離
  
  執行緒分為兩種狀態:可結合態和分離態;預設情況下,執行緒被建立成可結合的。

1. 可結合態:

  1. 這種狀態下的執行緒是能夠被其他程序回收其資源或殺死的,這句話我的理解是:與其說它能夠被其他程序回收或殺死,不如說它需要被其他程序回收或殺死;當它在被其他執行緒回收之前,它的儲存器資源(如棧)是不會釋放的;
  2. 這跟子程序很相似,如果不用父程序wait回收的話,就會變成殭屍程序同理,如果一個可結合態執行緒不用pthread_join回收,則會變成類似殭屍程序

2. 分離態

  1. 這種狀態下的執行緒是不能夠被其他執行緒回收或殺死的;它的儲存資源在它終止時由系統自動釋放
  2. 為了避免儲存器洩漏,每個可結合線程需要顯示的呼叫pthread_join回收;>要麼就將其變成分離態的執行緒

  6.6.2 執行緒分離函式—–pthread_detach

    #include <pthread.h>
    int pthread_detach(pthread_t thread);
    //將pthread_thread對應的執行緒設為分離態的執行緒

pthread_detach的兩種用法:

  1. 新執行緒中寫:pthread_detach(pthread_self());
  2. 主執行緒中寫:pthread_detach(thread);

注:多個執行緒,是在同一個程序中的,它們都共享著同一塊虛擬空間,第一種方法是將自己從這些執行緒中分離出來;
第二種方法是將指定的執行緒從這些執行緒中分離出去;簡單來說就是,一個是自己把自己弄出去,一個是讓別人把自己弄出去