在上一篇我們了解了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變量開銷更小。

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;
}
我們來檢視一下目前的線程
1.2.2 代碼相關解釋
首先我們建立了一個線程id(tid),這個tid是一個整數,我們來看看線程id是什麼?
我們列印完發現這個tid怎麼這麼大,這裡所謂的id是什麼?這個值是什麼?我們到後面會說。現在我們可以先把這個數字轉成16進制看看。
//将tid轉乘16進制
static void printTid(const pthread_t& tid)
{
printf("tid: 0x%x\n",tid);
}
庫内我們還有一個可以擷取自己id的函數pthread_self(). -- 誰掉這個函數就把線程id傳回給誰
是以我們修改一下代碼,目前在擷取一下線程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;
}
很明顯這兩個線程的tid是不一樣的。
1.2.3 建立多個線程
現在我們已經學會了建立線程了,那麼我們如果建立多個線程呢?
我們可以和建立多程序一樣,可以打循環建立。
1.2.4 線程ID及程序位址空間布局
- pthread_create函數會産生一個線程ID,存放在第一個參數指向的位址中。該線程ID和前面說的線程ID不是一回事。
- 前面講的線程ID屬于程序排程的範疇。因為線程是輕量級程序,是作業系統排程器的最小機關,是以需要一個數值來唯一表示該線程。
- pthread_create函數第一個參數指向一個虛拟記憶體單元,該記憶體單元的位址即為新建立線程的線程ID,屬于NPTL線程庫的範疇,線程庫的後續操作,就是根據線程ID來操作線程的。
- 線程庫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其實就是一個無符号長整型的整數。
1.3 線程等待
我們在主執行流(main)内建立線程之後,我們也要等待線程,類似于程序部分的父程序等待子程序。不等待的話可能會引發記憶體洩漏問題。
1.3.1為什麼需要線程等待
為什麼要有線程等待主要有兩點原因:
- 已經退出的線程,其空間沒有被釋放,仍然在程序的位址空間内。
- 建立新的線程不會複用剛才退出線程的位址空間
是以有可能引發記憶體洩漏的問題。
1.3.2 pthread_join介紹及其編碼
如何等待一個線程呢?我們可以使用pthread_join函數,首先我們先了解一下這個函數
- 功能:等待線程結束
- 原型:int pthread_join(pthread_t thread, void **value_ptr);
- 參數
- thread:線程ID
- value_ptr:它指向一個指針,後者指向線程的傳回值
- 傳回值:成功傳回0,失敗傳回錯誤碼。
#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
是以線程退出的時候必須要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;
}
我們能夠發現主線程是可以收到線程的退出碼的。
1.4 線程終止
如果需要隻終止某個線程而不終止整個程序,可以有三種方法:
- 從線程函數return,這種方法對于主線程不适用,從mian函數return相當于調用exit。
- 線程可以調用pthread_exit終止自己
- 一個線程可以調用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配置設定的,不能是線程函數在棧上配置設定,因為當其他線程得到這個傳回指針時線程函數已經退出了。有可能造成野指針問題。
#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;
}
1.4.2 pthread_cancel 介紹和編碼
這個方法不太常用,但是還是介紹一下
pthread_cancel
- 功能:取消一個執行中的線程
- 原型:int pthread_cancel(pthread_t thread);
- 參數:
- thread :線程ID
- 傳回值:成功傳回0,失敗傳回錯誤碼。
#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;
}
我們發現傳回的結果是-1,這裡我們需要知道如果線程是被取消的,退出結果是:-1。
-1 是庫裡面給我提供的一個宏
#define PTHREAD_CANCELED ((void *) -1)
1.5 線程控制總結
至此我們了解了線程的建立,線程的等待,以及線程終止的三種方式。線上程等待中,我們要調用pthread_join函數,調用該函數的線程将挂起等待,直到id為thread的線程終止。thread線程以不同的方法終止,通過pthread_join得到的終止狀态是不同的,總結如下:
- 如果thread線程通過return傳回,value_ ptr所指向的單元裡存放的是thread線程函數的傳回值。
- 如果thread線程被别的線程調用pthread_ cancel異常終掉,value_ ptr所指向的單元裡存放的是常數-1(PTHREAD_ CANCELED)
- 如果thread線程是自己調用pthread_exit終止的,value_ptr所指向的單元存放的是傳給pthread_exit的參數。
- 如果對thread線程的終止狀态不感興趣,可以傳NULL給value_ ptr參數。
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;
}