前面一篇我們已經(jīng)談及主線程和子線程之間的關(guān)系,以及線程在運(yùn)行時的線程狀態(tài),本篇我會討論到如何優(yōu)雅地連接線程溃蔫,并且通過一個具體的示例來結(jié)合前一篇所說的線程狀態(tài)來分析不合理使用連接線程帶來的負(fù)面影響姊扔。
線程的連接
我們通過man命令查看一下pthread_join的文檔
int pthread_join(pthread_t thread, void **retval);
- 參數(shù)thread 就是傳入線程的ID
- 參數(shù)retval 就是傳入線程的返回碼盔夜,如果線程被取消怎燥,那么rval被重置為PTHREAD_CANCELED, 如果調(diào)用成功瘫筐,返回0,失敗就返回大于0的整數(shù)。
再進(jìn)一步之前铐姚,我們需要了解一下前面沒有談及的線程屬性--分離
- 默認(rèn)情況下,pthread_create創(chuàng)建的進(jìn)程是非分離的,分離是指一個運(yùn)行時的線程的一個特定屬性肛捍,只是告知系統(tǒng)內(nèi)核該線程結(jié)束時隐绵,其使用的資源可以回收,其中包括釋放所有該線程結(jié)束時未釋放的系統(tǒng)的資源(包括返回值的內(nèi)存空間,堆拙毫,棧依许,寄存器等內(nèi)存空間)。
- 一個沒有被分離的線程在結(jié)束時缀蹄,系統(tǒng)內(nèi)核會保留它的虛擬內(nèi)存峭跳,當(dāng)中包括它們的堆和棧,寄存器(這種線程,通常叫僵尸線程缺前,那么該僵尸線程的宿主進(jìn)程蛀醉,自然就成為僵尸進(jìn)程)
還有其他線程屬性,請參考這個文檔
pthread_join調(diào)用注意事項
- 調(diào)用該系統(tǒng)調(diào)用會使傳入的線程擁有分離屬性衅码,如果該線程已經(jīng)處于分離的狀態(tài)拯刁,那么調(diào)用就會失敗。
- 調(diào)用該系統(tǒng)調(diào)用的線程會一直阻塞逝段,直到指定的線程(用線程ID來標(biāo)識)調(diào)用pthread_exit垛玻,從啟動的線程回調(diào)函數(shù)中返回或者調(diào)用pthread_cancel(傳入該線程的ID)
下面的例子是一個非常好的例子割捅,非常形象地解析Linux內(nèi)核的線程管理機(jī)制的,首先我們創(chuàng)建了一個代表非負(fù)整數(shù)序列的結(jié)構(gòu)體Seque帚桩,然后在1到200分成三個長度不同的數(shù)據(jù)區(qū)間亿驾,分別傳遞給三個線程。
#include <stdio.h>
#include <sys/types.h>
#include <unistd.h>
#include <stdlib.h>
#include <pthread.h>
#include<string.h>
typedef struct{
unsigned long begin;
unsigned long end;
unsigned long sum;
} Sequen;
void* calc(void* args){
Sequen* se=(Sequen*)args;
unsigned long beg=se->begin;
unsigned long end=se->end;
pthread_t tid=pthread_self();
//計算奇數(shù)和
for(unsigned long i=beg;i<=end;i+=2){
se->sum+=i;
}
printf("子線程0x%lx ->計算結(jié)果:%lu\n",tid,se->sum);
return NULL;
}
int main(int argc,char* argv){
pthread_t tids[3];
Sequen seq[3]={
{1,50,0},
{51,100,0},
{101,200,0}
};
int err;
for(int i=0;i<3;i++){
printf("創(chuàng)建地第%d個子線程\n",i+1);
err=pthread_create(&tids[i],NULL,calc,&seq[i]);
if (err){
printf("創(chuàng)建線程失敗!!\n");
continue;
}
pthread_join(tids[i],NULL);
}
printf("主線程計算結(jié)果:%ld\n",seq[0].sum+seq[1].sum+seq[2].sum);
}
輸出結(jié)果:你覺得很奇怪是嗎账嚎?為什么三個子線程的ID是一樣的呢颊乘?
我們分析一下這里的三個線程發(fā)生了什么事情,首先我們在for循環(huán)中分別使用pthread_create創(chuàng)先后創(chuàng)建了三個進(jìn)程(注意:字眼是“先后”代表是有順序)
- 第一個被創(chuàng)建的線程醉锄,系統(tǒng)從線程ID的編號空間中分配一個線程ID(十六進(jìn)制)0x7f1657e2e700給tids[0],隨后立即將該子線程傳遞給pthread_join處理,此時第一個線程創(chuàng)建之后乏悄,但未被pthread_join處理完之前,該線程的狀態(tài)是“運(yùn)行”了恳不。注意,這個phread_join是主線程調(diào)用的,因此主線程進(jìn)入“阻塞”狀態(tài),此時主線程只能等第一個子線程結(jié)束并且什么事都做不了檩小,因此也無法創(chuàng)建剩下的兩個子線程。
- 當(dāng)?shù)谝粋€線程終止后,主線程從“阻塞”回到“運(yùn)行”狀態(tài)烟勋,而且在第一個子線程結(jié)束后规求,系統(tǒng)內(nèi)核回收了第一個子線程ID:0x7f1657e2e700,并將該ID再“轉(zhuǎn)租”給第二個子線程。因此第一個子線程和第二個子線程的線程ID是一樣的卵惦。當(dāng)主線程再次調(diào)用pthread_join后阻肿,那么主線程再次從“運(yùn)行”狀態(tài)切換到“阻塞”狀態(tài),一直等到第二個子線程執(zhí)行完畢,才能接著創(chuàng)建第三個子線程沮尿。
- 同理丛塌,第三個子線程也和前面兩個步驟的分析一樣。
你因為這個分析不合理嗎畜疾?我也查閱了gnu官網(wǎng)的相關(guān)文檔說明https://www.gnu.org/software/libc/manual/html_node/Process-Identification.html
中文翻譯:
在Linux上赴邻,由pthread_create創(chuàng)建的線程也會收到線程ID。 初始(主)線程的線程ID與整個進(jìn)程的進(jìn)程ID相同啡捶。 隨后創(chuàng)建的線程的線程ID是不同的姥敛。 它們是從與進(jìn)程ID相同的編號空間分配的。 有時瞎暑,進(jìn)程ID和線程ID也統(tǒng)稱為任務(wù)ID彤敛。 與進(jìn)程相比,線程從不顯式等待了赌,因此線程ID在線程退出或取消后立即可以重用墨榄。 即使對于可連接線程,也不僅僅是分離線程揍拆,都是如此渠概。 線程被分配給線程組。 在Linux上運(yùn)行的GNU C庫實現(xiàn)中,進(jìn)程ID是進(jìn)程中所有線程的線程組ID播揪。
phread_join的副作用
OK贮喧,phread_join調(diào)用雖然能夠使改變目標(biāo)子線程的線程屬性,但它明顯的副作用是調(diào)用它的函數(shù)上下文所處的線程處于阻塞狀態(tài)猪狈,調(diào)用phread_join的線程無法執(zhí)行接下來的其他指令箱沦。
試問上面的示例跟一個單線程的同步調(diào)用沒什么區(qū)別!~因為主線程的間隔性的堵塞,令到子線程沒辦法連續(xù)創(chuàng)建和并發(fā)運(yùn)行雇庙。因此谓形,我們使用在多線程編程中,要謹(jǐn)慎使用phread_join調(diào)用疆前,我們應(yīng)該結(jié)合我們具體的上下問邏輯來合理使用phread_join系統(tǒng)調(diào)用.
Join vs Detach
這是本篇的主題寒跳,我們之前本篇開頭已經(jīng)說了pthread_detach能夠線程屬性改變?yōu)榉蛛x狀態(tài),在退出時自動釋放其分配的資源。 不需要其他線程需要連接它竹椒。 但是默認(rèn)情況下所有線程都是可連接(Joinable)童太,因此要分離一個線程,我們需要使用指定的線程ID傳遞給pthread_detach系統(tǒng)調(diào)用胸完,并且在主線程中調(diào)用它,我們上面的main線程书释,將pthread_joint替換成pthread_detach即可,如下代碼
int main(int argc,char* argv){
pthread_t tids[3];
Sequen seq[3]={
{1,50,0},
{51,100,0},
{101,200,0}
};
int err;
for(int i=0;i<3;i++){
printf("創(chuàng)建地第%d個子線程\n",i+1);
err=pthread_create(&tids[i],NULL,calc,&seq[i]);
if (err){
printf("創(chuàng)建線程失敗!!\n");
continue;
}
pthread_detach(tids[i]);
}
printf("主線程計算結(jié)果:%ld\n",seq[0].sum+seq[1].sum+seq[2].sum);
return 0;
}
我們編譯后嘗試運(yùn)行一下赊窥,從下面的輸出爆惧,三個線程ID是不一樣的證明子線程可以并發(fā)運(yùn)行了,但是主線程是提前return了锨能,因為整個程序計算的整數(shù)序列的輸出結(jié)果是不對的扯再。
那么究竟有沒有方法可以保證所以子線程可以并發(fā)運(yùn)行,同時主線程在確認(rèn)所有子線程返回計算結(jié)果后腹侣,主線程再執(zhí)行剩余的操作再返回叔收?你可能會想到pthread_exit調(diào)用嗎!非常抱歉pthread_exit在這里不適用傲隶,因為pthread_exit之后的所有語句都是不會再執(zhí)行的。
我們先將這個問題窃页,留給下一篇文章來解決.后續(xù)繼續(xù)更新....