Linux多線程

多線程

越深入的學習之後,經常能聽到這麼一個詞----多線程。之前的學習經常會提到多進程,父進程在忙不過來的情況下,會創建子進程進行幫忙,這樣就是一個多進程的任務。那麼什麼是多線程呢?

線程概念

在傳統的操作系統中,進程就是一個運行中程序的描述信息----pcb,控制程序的運行。

在Linux操作系統下,pcb是進程,因爲Linux下線程是以進程pcb模擬實現線程;也有人稱爲輕量級進程。但是Linux下沒有爲線程設計一個pcb來控制線程的運行。

線程1

上圖介紹了線程組是個什麼組合。

線程組理解

進程就是線程組,包含了一個或多個線程

從上圖來看,Linux線程是PCB,因爲CPU調度程序運行是調度pcb,所以線程是CPU調度的基本單位。

因爲進程是線程組,程序運行時,資源是分配給整個線程組的,因此線程是資源分配的基本單位。

vfork()創建一個子進程共用同一個虛擬地址空間,怕出現調用棧混亂,因此子進程運行完畢或者程序替換後父進程纔開始運行。

多線程和多進程的比較

多線程和多進程都可以並行多任務,那麼哪個執行起來比較好呢

從線程的角度來看,優點:

1、一個進程中的線程共用同一個虛擬地址空間

2、線程間通信更爲方便

3、線程的創建/銷燬成本更低

4、同一個進程間的線程調度成本要更低

5、執行力度更加細緻

缺點:

線程缺乏訪問控制----健壯性低。

比如exit(),異常針對的是整個進程,進程退出,那麼線程也就不存在了。這樣的話線程的可控性比較低

共同優點:都可以併發/並行處理任務,提高處理效率

多進程/多線程進行多任務處理的優勢體現與細節:

CPU密集型程序:程序中都是大量的運算操作

IO密集型程序:程序中都是大量的IO操作

共同缺點:對臨界資源操作需要考慮的更多,編碼更加複雜

線程創建,線程終止,線程等待,線程分離

先回顧一下進程創建。在之前的總結中我們通過fork()函數和vfork()函數創建子進程。

  • fork 是 創建一個子進程,並把父進程的內存數據copy到子進程中。

  • vfork是 創建一個子進程,並和父進程的內存數據share一起用。

    這兩個的區別是,一個是copy,一個是share。

    fork函數是在父進程執行到子進程創建的位置,將內存數據拷貝進入,等待子進程執行,子進程執行完後,退出後父進程在繼續執行。

    vfork函數是保證子進程先執行,當子進程調用exit()或exec()後,父進程往下執行

操作系統並沒有爲用戶提供直接創建線程的系統調用接口。但是大佬封裝了一套庫線程控制

用戶創建的進程是一個用戶態線程,在內核中對應了一個輕量級進程實現程序的調度運行

線程創建
#include <pthread.h>
//注意!!!
//注意!!!
//注意!!!
因爲是庫函數,所以編譯鏈接的時候需要加上-pthread/-lpthread鏈接線程庫
pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void (start_routine) (void *), void *arg);
pthread_t *thread	unsigned long int 輸出型參數,獲取線程的PID
const pthread_attr_t *attr,線程屬性,大部分時間都設爲NULL
void(start_routine)(void*)	線程入口函數
void* arg	通過線程入口函數傳遞給線程的參數	作爲實參傳給函數
返回值:成功返回 0,失敗返回 errno>0
    
pthread_create()函數在調用過程中啓動一個新線程。新線程通過調用start_routine()開始執行,arg作爲start_routine()的唯一參數傳遞。

pthread_t pthread_self(void);
返回調用線程ID;

線程創建Demo,最近正好學習了C++,將代碼都改成了C++的風格

  #include <iostream>                      
  #include <unistd.h>             
  #include <stdlib.h>
  #include <pthread.h>                                               
                   
  void * thr_start(void *arg){
    while(1){
      std::cout<<"I am child thread-----"<<(char*)arg<<std::endl;
      sleep(1);   
    }                                  
    return NULL;      
  }                                                               
  int main(int argc, char* argv[]){
    pthread_t tid;                                                   
    int ret = pthread_create(&tid,NULL,thr_start,(void*)"I am hujun!");
    if(ret != 0){
      std::cout<< "thread create error"<<std::endl;
      return -1; 
    }
    sleep(5);      
    pthread_cancel(tid);
    while(1){    
      std::cout<<"I am main thread "<< tid << std::endl;
      sleep(1);    
    }                   
    return 0;    
  } 

線程2

如圖爲運行結果

在新線程創建之後,執行線程函數中的操作。父進程休眠上五秒,此時在這五秒內,線程不斷執行操作。在這之後,時間結束後,線程退出,父進程繼續執行操作。

在打印結果中可以看到tid打印的是一串數字,爲什麼不是地址呢?

其實打印的tid是線程地址空間的首地址,也有其他的說法稱爲該線程的真實pid

通過指令ps -L查看輕量級進程

線程3

可以看到PID和LWP是相同的,這是因爲

此處的PID是線程組ID----tgid(thread group ID),可以理解爲線程組中主線程pid

此處的LWP顯示的就是線程ID,也可以說是tid。

線程4

線程終止

在線程入口函數中return;main函數中不能return,否則退出的是進程

void pthread_exit(void* retval);
退出調用線程,retval作爲返回值;
主動退出,誰調用誰退出;
int pthread_cancel(pthread_t thread);
取消一個指定的線程,被動退出;

主線程退出,進程並不會退出,線程退出也會成爲殭屍線程。線程地址空間無法被回收再利用,造成內存泄漏。

線程退出Demo

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

void *thr_start(void *arg){    
  	while(1){      
  		printf("child thread----\n");    
  		sleep(1);    
  		pthread_exit(NULL);    
  		}      
  	return NULL;      
  }    
      
int main(int argc , char *argv[]){    
    pthread_t tid;    
    int ret = pthread_create(&tid, NULL, thr_start,NULL);    
    if(ret != 0){    
      printf("thread create error\n");    
      return -1;    
    }    
    //使指定的線程退出tid == thread    
    pthread_cancel(tid);    
    while(1){    
      printf("main thread -------\n");    
      sleep(1);    
    }    
    return 0;                      
  } 

線程5

打印的結果如上。

如果根據代碼來看,可能會認爲線程創建完之後,應該先打印child pthread。但是卻不是這樣,這是因爲對於主函數來說線程創建完成後,它將繼續往下走,而重新創建的新線程需要去創建或尋找接口函數等一系列操作。當這些執行完成之後,纔打印線程的操作。

在代碼中有pthread_cancel這個函數的使用。線程取消的方法是向目標線程發Cancel信號(pthread_cancel函數發送Cancel信號),但如何處理Cancel信號則由目標線程自己決定,或者忽略、或者立即終止、或者繼續運行至Cancelation-point(取消點),由不同的Cancelation狀態(pthread_setcancelstate函數設置狀態)決定。

所以目標線程選擇了退出,之後繼續執行主函數的操作

線程等待

功能:等待線程退出,獲取指定線程的返回值,允許系統回收線程資源

pthread_join(pthread_t, void **retval);
要等待的線程id
retval	輸出型參數-用於獲取退出線程的返回值成功返回0,失敗返回一個非0值
返回值:0	失敗:!0---errno

**一個線程創建起來,默認有一個屬性:joinable。**關鍵!!!!

說明:

  • 調用線程將一直阻塞, 直到指定的線程調用pthread_exit, 從啓動例程返回或被取消.

  • 如果線程從它的啓動例程返回, rval_ptr包含返回碼.

  • 如果線程被取消, 由rval_ptr指定的內存單元置爲: PTHREAD_CANCELED.

  • 如果對返回值不關心, 可把rval_ptr設爲NULL.

線程分離

將線程的一個屬性從joinable設置爲detach屬性

功能:分離一個線程,線程退出後系統將自動回收資源;被分離的線程無法被等待,若是非要pthread_join則會直接報錯

man手冊說明:將線程標識的線程標記爲已分離。當分離的線程終止時,其資源將自動釋放回系統,而不需要另一個線程與終止的線程聯接。

處於detach屬性的線程,退出後資源直接自動被回收,這類線程不能被等待

int pthread_detach(pthread_t thread);
//thread 要被分離的線程ID
//功能:分離一個線程(設置線程的屬性從joinable->detach屬性),線程退出後系統將自動回收資源,被分離的線程無法被等待,若是非要pthread_join則會直接報錯返回
注意:線程被分離的前提是用戶不關心,線程的退出返回值

線程的分離,對於一個線程來說,任意線程再任意位置調用都可以!!!

用法:通常如果用戶對線程的返回值並不關心,則在創建線程之後直接分離線程或者在線程入口函數中第一時間分離自己

處於joinable狀態的線程退出後不會自動釋放資源需要被等待

處於detach狀態的線程退出後系統會自動回收資源並且不需要等待

//一個線程等待和線程分離的Demo
#include <iostream>
#include <unistd.h>
#include <errno.h>
#include <stdlib.h>
#include <pthread.h>
#include <stdio.h>
using std::cout;
using std::endl;

extern void *thr_start(void* arg){
    //pthread_detach(pthread_self());
    //pthread_exit((void*)"leihou");
    while(1){
      cout<<"i am thread"<<endl;
      sleep(1);
    }
    return NULL;
  }
  
int main(int argc, char* argv[]){
    pthread_t tid;    
    int ret = pthread_create(&tid, NULL, thr_start, NULL);    
    if(ret != 0){    
      cout<<"pthread create error"<<endl;    
      return -1;    
    }    
    sleep(1);    
   }
    return NULL;
  }
  
int main(int argc, char* argv[]){
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, thr_start, NULL);
    if(ret != 0){
      cout<<"pthread create error"<<endl;
      return -1;
    }
    sleep(1);
    char* retval = NULL;
    int err = pthread_join(tid,(void**)&retval);
    if(err == EINVAL){
      cout<<"thread can not be waited"<<endl;
    }
    //cout<<retval<<endl;
    printf("retval:%s\n",retval);
    while(1){
      cout<<"i am main thread"<<endl;
      sleep(1);
    }
    return 0;
  }

首先將線程創建函數的兩個接口註釋掉。

當線程沒有退出時,pthread_join函數沒有立即返回,主函數繼續執行,而且沒有retval返回值。

當添加pthread_exit()函數後,此時線程等待結束退出後立即返回retval返回值“leihou”

當添加pthread_detach()函數後,此時線程分離當前線程,此時將不再有線程等待這麼一個操作,返回值retval此時爲NULL,並且分離後資源交由系統回收,此時主函數繼續循環執行。

下一篇好好分析線程安全和各種鎖和消費者與生產者模型

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章