【Linux】多線程入門詳解


背景知識:

1.每次進程切換,都存在資源的保持和恢復動作,即上下文切換

2.進程的引入雖然可以解決多用戶的問題,但是進程頻繁切換的開銷會嚴重影響系統性能

3.同一個進程內部有多個線程,這些線程共享的是同一個進程的所有資源

4.通過線程可以支持一份應用程序內部的並發,免去了進程頻繁切換的開銷

5.線程的切換是輕量級的,所以可以保證足夠快

6.即使是單核計算機,也可以通過不停的在多個線程的指令間切換,從而造成多線程同時運行的效果

7.操作系統一般都有一些系統調用來讓一個函數運行成為一個新的線程

8.對於多線程來說,由於同一個進程空間中存在多個棧,任何一個空白區域填滿都會導致棧溢出

9.多線程與棧密切相關

 

一.線程創建與結束

相關函數:

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

線程創建函數

參數1:*thread,需要創建的線程ID指針

參數2:*attr,用來設置線程屬性

參數3:void*,線程運行函數的起始地址,頁就是告訴線程你的線程運行函數是哪一個

參數4:*arg,線程運行函數的參數

函數的返回值int是函數是否運行成功的結果,當返回值為0表示運行成功,-1表示運行失敗

當線程運行函數的參數不止一個時,需要將這些參數封裝成一個結構體傳進去

2)int pthread_join(pthread_t thread,void **retval)

調用線程等待thread線程運行結束,並且獲得thread線程的返回值

參數1:thread,被等待線程的線程ID

參數2:用來存儲thread線程的返回值

該函數一般是主線程調用,用來等待子線程運行完畢,函數的返回值int是函數是否運行成功的結果,當返回值為0表示運行成功,-1表示運行失敗

3)void pthread_exit(void *retval)

結束當前線程,並返回一個返回值

參數1:*retval,線程結束的返回值

一般pthread_exit和pthread_join配套使用,獲得子線程的返回值

樣例程序:

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

void* say_hello(void* args)
{
    cout<<"hello from thread"<<endl;
    pthread_exit((void*)666);
}

int main()
{
    pthread_t tid;
    int iRet=pthread_create(&tid,NULL,say_hello,NULL);
    if(iRet)
    {
        cout<<"pthread_create error:iRet="<<iRet<<endl;
        return iRet;
    }
    void *retval;
    iRet=pthread_join(tid,&retval);
    if(iRet)
    {
        cout<<"pthread_join error:iRet="<<iRet<<endl;
        return iRet;
    }
    cout<<(long)retval<<endl;
    return 0;
}
/*
hello from thread
666
*/

先創建並運行一個子線程,在主線程中等待子線程運行結束,並且獲取子線程的返回值然后輸出

ps:調用pthread_join函數,獲取線程的返回值!

 

二.向線程傳遞參數

在創建線程的時候可以向線程傳遞參數,pthread_create函數的第四個參數即為線程運行函數的參數,當要傳遞的參數有多個時,需要將這些參數封裝起來然后傳遞

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

void* say_hello(void* args)
{
    int x=*(int*)args;
    cout<<"hello from thread,x="<<x<<endl;
    pthread_exit((void*)666);
}

int main()
{
    pthread_t tid;
    int para=123;
    int iRet=pthread_create(&tid,NULL,say_hello,&para);
    if(iRet)
    {
        cout<<"pthread_create error:iRet="<<iRet<<endl;
        return iRet;
    }
    void *retval;
    iRet=pthread_join(tid,&retval);
    if(iRet)
    {
        cout<<"pthread_join error:iRet="<<iRet<<endl;
        return iRet;
    }
    cout<<(long)retval<<endl;
    return 0;
}

/*
hello from thread,x=123
666
*/

 

三.獲取線程的ID

1)調用pthread_self函數來獲取當前運行線程的id,該函數的返回值是當前運行線程的id

2)在創建線程時直接獲取創建的線程的id

 

四.線程的屬性

typedef struct  
{  
    int                   etachstate;      //線程的分離狀態  
    int                   schedpolicy;     //線程調度策略  
    structsched_param     schedparam;      //線程的調度參數  
    int                   inheritsched;    //線程的繼承性  
    int                   scope;           //線程的作用域  
    size_t                guardsize;       //線程棧末尾的警戒緩沖區大小  
    int                   stackaddr_set;   //線程的棧設置  
    void*                 stackaddr;       //線程棧的位置  
    size_t                stacksize;       //線程棧的大小  
}pthread_attr_t;

1)線程的分離狀態:線程的分離狀態決定一個線程以什么樣的方式的來終止自己

1.非分離狀態:線程的默認屬性是非分離狀態,這種情況下,父線程等待子線程結束,只有當pthread_join函數返回時,子線程才算終止,才能釋放自己占用的系統資源

2.分離狀態:分離線程沒有被其他線程所等待,自己運行結束了,線程也就終止了,馬上釋放系統資源,可以根據自己的需要,選擇適當的分離狀態

3.怎么使得線程分離?

方法1:直接將線程設置為分離線程

pthread_attr_setdetachstate(pthread_attr_t *attr, int detachstate)

第二個參數可選為:PTHREAD_CREATE_DETACHED(分離線程)和PTHREAD_CREATE_JOINABLE(非分離線程)

這里需要注意一點的是,如果設置一個線程為分離狀態,而這個線程又運行得非常快,它很可能在pthread_create函數返回之前就終止了,它終止以后就可能將線程號和資源交給其他線程使用,這樣調用pthread_create就得到了錯誤的線程號,要避免這種情況可以采用一定的同步措施,比如在被創建的線程的運行函數中調用pthread_cond_timewait函數,使得線程睡眠幾秒,留出足夠的時間讓pthread_create返回,設置一段等待時間,這是多線程編程中常見的方法,但是注意不要使用諸如wait的函數,他們是使得整個進程睡眠,並不能解決線程同步問題!

方法2:在需要分離的線程的運行函數中調用pthread_detached函數

int pthread_detach(pthread_t tid);若成功則返回0,若出錯則為非零。

pthread_detach用於分離可結合線程tid。線程能夠通過以pthread_self()為參數的pthread_detach調用來分離它們自己。

即:pthread_detach(pthread_self())

 

2)線程的棧地址

當進程棧地址空間不夠時,,指定新建線程使用malloc分配的空間作為自己的棧空間,通過pthread_attr_setstackaddr和pthread_attr_getstackaddr兩個函數分別設置和獲取線程的棧地址,傳給pthread_addr_setstackaddr函數的地址是緩沖區的低地址(不一定是棧的開始地址,棧可能從高地址往低地址增長)

 

3)線程的棧大小

1.當系統中有很多線程時,可能需要減小每個線程的棧空間默認大小,防止進程的地址空間不夠用

2.當線程調用的函數會分配很大的局部變量或者函數調用層次很深時,可能需要增加線程默認棧的大小

3.函數pthread_attr_getstacksize和 pthread_attr_setstacksize提供設置。

 

4)線程的棧保護區大小

1.在線程棧的棧頂預留一段空間,防止棧溢出

2.當棧指針進入這段保護區時,系統會發出錯誤,通常是發送信號給線程

3.該屬性的默認值是PAGESIZE大小,該屬性被設置時,系統會自動將該屬性大小補齊為頁大小的整數倍

4.當棧改變地址屬性時,棧保護區大小通常清零

 

5)線程的優先級

1.新線程不繼承父進程的優先級,新線程的優先級默認為0

2.新線程使用SCHED_OTHER調度策略,即:線程一旦開始運行,直到被搶占或者直到線程阻塞或停止為止

 

6)線程的爭用范圍

1)PTHREAD_SCOPE_SYSTEM:此線程與系統中的所有線程進行競爭

2)PTHREAD_SCOPE_PROCESS:此線程與進程中的其他線程進行競爭

具有不停爭用范圍的線程可以在同一個線程甚至同一個進程中共存,進程范圍只允許這種與同一進程中的其他線程爭用資源,而系統范圍則允許此類線程與系統中其他所有線程爭用資源

 

7)線程的綁定狀態

輕進程:可以理解為內核線程,位於用戶層和系統層之間,系統對線程資源的分配,對線程的控制都是通過輕進程實現的,一個輕進程可以控制一個或多個線程

1.非綁定狀態

默認情況下,啟動多少個輕進程,哪些輕進程來控制哪些線程都是由系統來控制的,這種情況稱之為非綁定

2.綁定狀態

即某個線程綁定在一個輕進程之上,被綁定的線程具有較高的響應速度,這是因為CPU時間片的調度是面向輕進程的,綁定的線程可以保證在需要的時候它總有一個輕進程可用,通過被綁定的輕進程的優先級和調度級可用使得綁定的線程滿足諸如實時反應之類的要求

 

五.線程分離

1.將線程設置為結束狀態分離后,線程的結束狀態不能被進程中的其他線程得到,同時保存線程結束狀態的存儲區也將變得不能應用

樣例1:

在一個線程被創建之前分離線程

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

using namespace std;
void * thread_run(void * arg)
{
    cout<<"the thread"<<endl;
    return NULL;
}

int main(void)
{
    int flag;
    pthread_t tid;
    pthread_attr_t attr;//attrbute

    flag=pthread_attr_init(&attr);//thread init
    if(flag)
    {
      cout<<"can‘t init attr "<<strerror(flag)<<endl;
      return flag;
    }

    flag=pthread_attr_setdetachstate(&attr,PTHREAD_CREATE_DETACHED);//setting thread detachment status
    if(flag)
    {
      cout<<"can't set attr "<<strerror(flag)<<endl;
      return flag;
    }

    flag=pthread_create(&tid,&attr,thread_run,NULL);
    if(flag)
    {
      cout<<"can't create thread "<<strerror(flag)<<endl;
      return flag;
    }

    flag=pthread_join(tid,NULL);
    if(flag)
    {
      cout<<"can't join thread and thread has been detached"<<endl;
      return flag;
    }
    return 0;
}
//answer:can't join thread and thread has been detached

分析:由於子線程分離,因此得不到子線程的結束狀態信息,pthread_join函數會出錯,並且由於子線程分離,子線程運行函數中輸出的字符對主線程而言也是不可見的,所以字符串”the thread“沒有被打印出來!

樣例二:分離一個已經創建的線程

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

using namespace std;
void * thread_run(void * arg)
{
    cout<<"the son thread sleeping for 5 seconds"<<endl;
    sleep(5);//sleeping 5 seconds and wait fotr the main thread to set the thread to a separate state
    cout<<"the son thread done"<<endl;
    return NULL;
}

int main(void)
{
    int flag;
    pthread_t tid;

    flag=pthread_create(&tid,NULL,thread_run,NULL);
    if(flag)
    {
      cout<<"can't create thread "<<strerror(flag)<<endl;
      return flag;
    }

    flag=pthread_detach(tid);//detach a thread
    if(flag)
    {
      cout<<"can't detach thread "<<strerror(flag);
      return flag;
    }

    flag=pthread_join(tid,NULL);
    if(flag)
    {
      cout<<"can't join thread and thread has been detached"<<endl;
    }

    cout<<"the main thread sleeping for 8 seconds "<<endl;
    sleep(8);
    cout<<"the main thread done"<<endl;
    
    return 0;
}
/*
answer:
can't join thread and thread has been detached
the main thread sleeping for 8 seconds 
the son thread sleeping for 5 seconds
the son thread done
the main thread done
*/

分析:采用pthread_detach函數分離一個已經創建的線程,子線程睡眠5秒等待主線程將其分離!,這樣導致pthread_join函數取不到子線程結束的狀態信息

 

六.線程結束的四種方法

1.在線程運行函數中直接return(推薦)

在return之后,會清理函數內申請的對象,可以避免線程內存泄漏

2.調用pthread_exit(x)【返回x】,采用函數的形式則不會調用return函數,所以不會調用線程作用域內申請的對象的析構函數,會造成內存泄漏

3.使用同一進程中的其他線程終止線程(被動終止),pthread_cancel函數(使用不當會產生死鎖,比如通知一個在等待隊列中但是又被取消了的線程)

4.終止該線程所在的進程,調用exit()或者主線程return

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM