天天看點

[ Linux ] 線程控制(線程建立,等待,終止)

在上一篇我們了解了Linux下線程的相關概念。而本篇的主要内容是線程控制。線程控制包括線程的建立,線程的終止,線程等待等問題,以及線程分離和Linux常見線程安全問題。

1.線程控制

線程控制和我們之前學習過的程序控制類似,包括線程建立終止等待。我們會從完成編碼和驗證兩個方面完善線程控制。

1.1POSIX線程庫

在上一篇博文我們就提到過,作業系統并沒有直接提供相關的接口。而是由大多數程式員為我們開辟好了一個原生的線程庫。是以我們要對線程進行控制。就要進入這個線程庫。

  • 與線程有關的函數構成了一個完整的系列,絕大多數函數的名字都是以“pthread_”打頭的。例如pthread_create,pthread_join。
  • 要使用這些函數庫,要通過引入頭檔案<pthread.h>
  • 連結這些線程函數庫時要使用編譯器指令的“-lpthread”選項

1.2 建立線程

pthread_create

  • 功能:建立一個新的線程
  • 原型

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

  • 參數
  • thread:傳回線程id
  • attr:設定線程的屬性,attr為NULL表示使用預設屬性
  • start_routine:是個函數位址,線程啟動後要執行的函數(回調函數)
  • arg:傳給線程啟動函數的參數
  • 傳回值:成功傳回0,失敗傳回錯誤碼
  • 錯誤檢查:
  • 傳統的一些函數是成功傳回0,失敗傳回-1,并且對全局變量errno指派以訓示錯誤
  • pthreads函數出錯時不會設定全局變量errno(而大部分其他POSIX函數會這樣)。而是将錯誤代碼通過傳回值傳回
  • pthreads同樣也提供了線程内的errno變量,以支援其他使用errno的代碼。對于pthreads函數的錯誤,建議通過傳回值判定,因為讀取傳回值要比讀取線程内的errno變量開銷更小。
[ Linux ] 線程控制(線程建立,等待,終止)
[ Linux ] 線程控制(線程建立,等待,終止)

1.2.1 建立線程編碼

了解了線程建立的函數和參數使用方式之後,我們在代碼中來展現一番:

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

void *startRoutine(void *args)
{
    while(true)
    {
        cout<<"線程正在運作......"<<endl;
        sleep(1);
    }
    return nullptr;
}


int main()
{
    //建立tid
    pthread_t tid;

    //建立線程
    int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
    
    //主線程
    while(true)
    {
        cout<<"我是主線程,我正在運作......"<<endl;
        sleep(1);
    }
    return 0;
}      
[ Linux ] 線程控制(線程建立,等待,終止)

我們來檢視一下目前的線程

[ Linux ] 線程控制(線程建立,等待,終止)

1.2.2 代碼相關解釋

首先我們建立了一個線程id(tid),這個tid是一個整數,我們來看看線程id是什麼?

[ Linux ] 線程控制(線程建立,等待,終止)

我們列印完發現這個tid怎麼這麼大,這裡所謂的id是什麼?這個值是什麼?我們到後面會說。現在我們可以先把這個數字轉成16進制看看。

//将tid轉乘16進制
static void printTid(const pthread_t& tid)
{
    printf("tid: 0x%x\n",tid);

}      
[ Linux ] 線程控制(線程建立,等待,終止)

庫内我們還有一個可以擷取自己id的函數pthread_self(). -- 誰掉這個函數就把線程id傳回給誰

[ Linux ] 線程控制(線程建立,等待,終止)

是以我們修改一下代碼,目前在擷取一下線程id

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

//将tid轉乘16進制
static void printTid(const char *name,const pthread_t& tid)
{
    printf("%s 正在運作 ,tid: 0x%x\n",name,tid);

}
void *startRoutine(void *args)
{
    const char *name = static_cast<const char *>(args);
    while(true)
    {
        printTid(name,pthread_self());
        //cout<<"線程正在運作......"<<endl;
        sleep(1);
    }
    return nullptr;
}


int main()
{
    //建立tid
    pthread_t tid;
    //建立線程
    int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
    //printTid(tid);
    //cout<<"tid :" <<tid<<endl;
    //主線程
    while(true)
    {
        printTid("man thread:",pthread_self());
        //cout<<"我是主線程,我正在運作......"<<endl;
        sleep(1);
    }
    return 0;
}      
[ Linux ] 線程控制(線程建立,等待,終止)

很明顯這兩個線程的tid是不一樣的。

1.2.3 建立多個線程

現在我們已經學會了建立線程了,那麼我們如果建立多個線程呢?

我們可以和建立多程序一樣,可以打循環建立。

1.2.4 線程ID及程序位址空間布局

  1. pthread_create函數會産生一個線程ID,存放在第一個參數指向的位址中。該線程ID和前面說的線程ID不是一回事。
  2. 前面講的線程ID屬于程序排程的範疇。因為線程是輕量級程序,是作業系統排程器的最小機關,是以需要一個數值來唯一表示該線程。
  3. pthread_create函數第一個參數指向一個虛拟記憶體單元,該記憶體單元的位址即為新建立線程的線程ID,屬于NPTL線程庫的範疇,線程庫的後續操作,就是根據線程ID來操作線程的。
  4. 線程庫NPTL提供了pthread_self()函數,可以獲得線程自身的ID。 pthread_t pthread_self(void);

1.2.5 pthread_t

pthread_t 到底是什麼類型呢?取決于實作。對于Linux目前實作的NPTL實作而言,pthread_t類型的線程ID,本質就是一個程序位址空間上的一個位址。

我們在vs code中檢視pthread_t的類型可以發現,目前的pthread_t其實就是一個無符号長整型的整數。

[ Linux ] 線程控制(線程建立,等待,終止)
[ Linux ] 線程控制(線程建立,等待,終止)

1.3 線程等待

我們在主執行流(main)内建立線程之後,我們也要等待線程,類似于程序部分的父程序等待子程序。不等待的話可能會引發記憶體洩漏問題。

1.3.1為什麼需要線程等待

為什麼要有線程等待主要有兩點原因:

  1. 已經退出的線程,其空間沒有被釋放,仍然在程序的位址空間内。
  2. 建立新的線程不會複用剛才退出線程的位址空間

是以有可能引發記憶體洩漏的問題。

1.3.2 pthread_join介紹及其編碼

如何等待一個線程呢?我們可以使用pthread_join函數,首先我們先了解一下這個函數

  • 功能:等待線程結束
  • 原型:int pthread_join(pthread_t thread, void **value_ptr);
  • 參數
  • thread:線程ID
  • value_ptr:它指向一個指針,後者指向線程的傳回值
  • 傳回值:成功傳回0,失敗傳回錯誤碼。
[ Linux ] 線程控制(線程建立,等待,終止)
[ Linux ] 線程控制(線程建立,等待,終止)
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

//将tid轉乘16進制
static void printTid(const char *name,const pthread_t& tid)
{
    printf("%s 正在運作 ,tid: 0x%x\n",name,tid);

}
void *startRoutine(void *args)
{
    const char *name = static_cast<const char *>(args);
    int cnt = 5;
    while(true)
    {
        printTid(name,pthread_self());
        //cout<<"線程正在運作......"<<endl;
        sleep(1);
        if(!(cnt--))  break;
    }
    cout<<"線程退出啦........"<<endl;
    return nullptr;
}


int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");

    sleep(10);

    pthread_join(tid,nullptr);
    return 0;
}      

我們再寫一個監控腳本 檢視目前線程個數

while :; do ps -aL | head -1 && ps -aL | grep mythread; sleep 1; done

[ Linux ] 線程控制(線程建立,等待,終止)

是以線程退出的時候必須要join,如果不join就會造成類似于程序那樣的記憶體洩漏問題。

1.3.3 join的第二個參數 value_ptr

我們檢視文檔發現,join的第二個參數是一個二級指針,而且是一個輸出型參數,指向的是線程的傳回值。我們所寫的回調函數的傳回值是一個void*,如果我們傳回一個void*的值,主線程可以接受的。我們使用編碼來進行驗證。

#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;


//将tid轉乘16進制
static void printTid(const char *name,const pthread_t& tid)
{
    printf("%s 正在運作 ,tid: 0x%x\n",name,tid);


}
void *startRoutine(void *args)
{
    const char *name = static_cast<const char *>(args);
    int cnt = 5;
    while(true)
    {
        printTid(name,pthread_self());
        //cout<<"線程正在運作......"<<endl;
        sleep(1);
        if(!(cnt--))  
        {
            break;
            // int *p = nullptr;
            // *p = 100;//野指針問題
        }
    }
    cout<<"線程退出啦........"<<endl;
    return (void*)111;
}



int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
    (void)n;


    void *ret = nullptr;// void* -> 位元組
    pthread_join(tid,&ret);//void **retval是一個輸出型參數
    cout<<"main thread join sucess , *ret:" <<(long long)ret<<endl; 

    sleep(10);
    //主線程
    while(true)
    {
        printTid("man thread:",pthread_self());
        //cout<<"我是主線程,我正在運作......"<<endl;
        sleep(1);
    }


    return 0;
}      
[ Linux ] 線程控制(線程建立,等待,終止)

我們能夠發現主線程是可以收到線程的退出碼的。

1.4 線程終止

如果需要隻終止某個線程而不終止整個程序,可以有三種方法:

  1. 從線程函數return,這種方法對于主線程不适用,從mian函數return相當于調用exit。
  2. 線程可以調用pthread_exit終止自己
  3. 一個線程可以調用pthread_cancel終止同一程序中的另一個線程

其中方法一正是我們1.3.3所提到的。這裡我們再來了解剩下兩個函數,線程調用pthread_exit()函數終止自己和線程調用pthread_cancel終止同程序内的另一個線程。

1.4.1pthread_exit 介紹和編碼

pthread_exit函數

  • 功能:線程終止
  • 原型:void pthread_exit(void *value_ptr);
  • 參數:value_ptr:value_ptr不要指向一個局部變量
  • 傳回值:無傳回值,跟程序一樣,線程結束的時候無法傳回到它的調用者(自身)

需要注意的是,pthread_exit或者return傳回的指針所指向的記憶體單元必須是全局的或者是用malloc配置設定的,不能是線程函數在棧上配置設定,因為當其他線程得到這個傳回指針時線程函數已經退出了。有可能造成野指針問題。

[ Linux ] 線程控制(線程建立,等待,終止)
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

//将tid轉乘16進制
static void printTid(const char *name,const pthread_t& tid)
{
    printf("%s 正在運作 ,tid: 0x%x\n",name,tid);

}
void *startRoutine(void *args)
{
    const char *name = static_cast<const char *>(args);
    int cnt = 5;
    while(true)
    {
        printTid(name,pthread_self());
        //cout<<"線程正在運作......"<<endl;
        sleep(1);
        if(!(cnt--))  
        {
            break;
            // int *p = nullptr;
            // *p = 100;//野指針問題
        }
    }
    cout<<"線程退出啦........"<<endl;
    //return (void*)111;
    //pthread_exit
    pthread_exit((void*)2222);
}


int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
    (void)n;

    void *ret = nullptr;// void* -> 位元組
    pthread_join(tid,&ret);//void **retval是一個輸出型參數
    cout<<"main thread join sucess , *ret:" <<(long long)ret<<endl; 

    return 0;
}      
[ Linux ] 線程控制(線程建立,等待,終止)

1.4.2 pthread_cancel 介紹和編碼

這個方法不太常用,但是還是介紹一下

pthread_cancel

  • 功能:取消一個執行中的線程
  • 原型:int pthread_cancel(pthread_t thread);
  • 參數:
  • thread :線程ID
  • 傳回值:成功傳回0,失敗傳回錯誤碼。
[ Linux ] 線程控制(線程建立,等待,終止)
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

//将tid轉乘16進制
static void printTid(const char *name,const pthread_t& tid)
{
    printf("%s 正在運作 ,tid: 0x%x\n",name,tid);

}
void *startRoutine(void *args)
{
    const char *name = static_cast<const char *>(args);
    while(true)
    {
        printTid(name,pthread_self());
        //cout<<"線程正在運作......"<<endl;
        sleep(1);
    }
    cout<<"線程退出啦........"<<endl;
    //return (void*)111;
    //pthread_exit((void*)2222);
}


int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");
    (void)n;

    sleep(3);//代表main thread對應的工作
    //3.給線程發送取消請求 如果線程是被取消的,退出結果是:-1
    pthread_cancel(tid);
    cout<<"new thread been canceled"<<endl;

    void *ret = nullptr;// void* -> 位元組
    pthread_join(tid,&ret);//void **retval是一個輸出型參數
    cout<<"main thread join sucess , *ret:" <<(long long)ret<<endl; 
    
    return 0;
}      
[ Linux ] 線程控制(線程建立,等待,終止)

我們發現傳回的結果是-1,這裡我們需要知道如果線程是被取消的,退出結果是:-1。

-1 是庫裡面給我提供的一個宏

#define PTHREAD_CANCELED ((void *) -1)

[ Linux ] 線程控制(線程建立,等待,終止)

1.5 線程控制總結

至此我們了解了線程的建立,線程的等待,以及線程終止的三種方式。線上程等待中,我們要調用pthread_join函數,調用該函數的線程将挂起等待,直到id為thread的線程終止。thread線程以不同的方法終止,通過pthread_join得到的終止狀态是不同的,總結如下:

  1. 如果thread線程通過return傳回,value_ ptr所指向的單元裡存放的是thread線程函數的傳回值。
  2. 如果thread線程被别的線程調用pthread_ cancel異常終掉,value_ ptr所指向的單元裡存放的是常數-1(PTHREAD_ CANCELED)
  3. 如果thread線程是自己調用pthread_exit終止的,value_ptr所指向的單元存放的是傳給pthread_exit的參數。
  4. 如果對thread線程的終止狀态不感興趣,可以傳NULL給value_ ptr參數。
[ Linux ] 線程控制(線程建立,等待,終止)

1.6 驗證 線程異常問題

至此我們了解了線程建立和線程等待,那麼如果線程異常了會怎麼辦呢?這和線程的健壯性相關,在上篇線程介紹中我們提到過線程異常,我們當時說

  • 單個線程如果出現除零,野指針問題導緻線程崩潰,程序也會随着崩潰
  • 線程是程序的執行分支,線程出異常,就類似程序出異常,進而觸發信号機制,終止程序,程序終止,該程序内的所有線程也就随即退出
#include <iostream>
#include <unistd.h>
#include <pthread.h>
using namespace std;

//将tid轉乘16進制
static void printTid(const char *name,const pthread_t& tid)
{
    printf("%s 正在運作 ,tid: 0x%x\n",name,tid);

}
void *startRoutine(void *args)
{
    const char *name = static_cast<const char *>(args);
    int cnt = 5;
    while(true)
    {
        printTid(name,pthread_self());
        //cout<<"線程正在運作......"<<endl;
        sleep(1);
        if(!(cnt--))  
        {
            int *p = nullptr;
            *p = 100;//野指針問題
        }
    }
    cout<<"線程退出啦........"<<endl;
    return nullptr;
}


int main()
{
    pthread_t tid;
    int n = pthread_create(&tid,nullptr,startRoutine,(void*)"thread 1");


    pthread_join(tid,nullptr);

    sleep(10);
    while(true)
    {
        printTid("man thread:",pthread_self());
        //cout<<"我是主線程,我正在運作......"<<endl;
        sleep(1);
    }
    return 0;
}      

繼續閱讀