1. 程式人生 > >多執行緒基礎操作

多執行緒基礎操作

本片部落格會貼上部分程式碼,想要了解更多程式碼資訊,可訪問小編的GitHub關於本篇的程式碼

- 對比程序與執行緒的區別

執行緒概念及特點

  1. Linux下執行緒是以程序模擬的,Linux下的程序控制塊pcb實際就是一個執行緒,其他系統不一定
  2. Linux對程序和執行緒不作區分,Linux下的執行緒以程序PCB模擬,也就是說task_struct其實就是執行緒CPU排程的基本單位是執行緒,程序就是執行緒組
  3. Linux下的pcb其實就是執行緒的描述,Linux下的執行緒以程序pcb模擬,因此也叫輕量級程序。
  4. 一個程序中至少有一個執行緒,執行緒是程序內的一條執行流。
執行緒共享程序資料,但也擁有自己的部分資料:

執行緒ID
組暫存器(上下文資料)

errno
訊號遮蔽字
排程優先順序

如果定義1個函式,在各執行緒中都可以呼叫,如果定義1個全域性變數,在各執行緒中都可以訪問到,除此之外,各執行緒還共享以下程序資源和環境:

檔案描述符表
每種訊號的處理方式(SIG_ IGN、SIG_ DFL或者?定義的訊號處理函式)
當前工作目錄
使用者id和組id
Text Segment、Data Segment都是共享的。

程序和執行緒的比較,執行緒優缺點

執行緒的優點:
一個程序中有可能會有多個執行緒,而這些執行緒共享同一個虛擬地址空間,因此有時候也會說執行緒是在程序中的

a、 因此他們共享了整個程式碼段、資料段,執行緒間通訊變得極為方便
b、建立或銷燬一個執行緒相比較於程序來說成本更低(不需要額外建立虛擬地址空間)
c、執行緒的排程切換相較於程序也較低
d、執行緒佔用的資源比程序少很多
e、能夠充分利用多處理器的可並行數量
f、在等待IO操作、CPU計算等任務時候,執行緒支援多處理器並行處理,任務分攤,提高效率。

執行緒的缺點:一個程序中有可能會有多個執行緒,而這些執行緒共享同一個虛擬地址空間

a、 因為執行緒間的資料訪問變得簡單,因此資料安全訪問問題更加突出,要考慮的問題增多,編碼難度增多。資源爭搶問題更加突出。
b、 一些系統呼叫和異常都是針對整個程序的,因此一個執行緒中出現了異常,那麼,整個程序都會受到影響

,以及一些系統呼叫的使用也是需要注意的。

程序的優點:安全、穩定(因為程序的獨立性)

- 執行緒相關程式碼,總結執行緒屬性

執行緒的控制

程序是作業系統資源分配的一個基本單位(通過頁表)
執行緒是CPU排程的一個基本單位(CPU排程的是pcb)

執行緒建立:執行緒共享程序地址空間,但是每一個執行緒都有自己相對獨立的一個地址空間

作業系統並沒有提供系統呼叫來建立執行緒,所以就在posix標準庫中,實現了一套執行緒控制(建立、終止、等待…)的介面,因為這套介面建立的執行緒是庫函式,是使用者態的執行緒建立,因此建立的執行緒也叫做使用者執行緒。 一個使用者執行緒對應了一個輕量級程序來進行排程執行的。(對作業系統來說,建立了一個輕量級程序)

int pthread_create(pthread_t *thread, const pthread_attr_t *attr,
                          void *(*start_routine) (void *), void *arg);
Compile and link with -pthread.

pthread_t * thread:建立的執行緒id
const pthread_attr_t:執行緒屬性,一般設定為NULL
void *(*start_routine) (void *):執行緒的執行函式
void *arg:給執行緒的執行函式傳的引數

pthread_create介面建立了一個使用者執行緒,並且通過第一個引數返回了一個使用者的id,這個id數字非常大,其實它就是一個地址,是指向自己的執行緒地址空間在整個程序虛擬地址空間中的位置。
每一個執行緒都需要有自己的棧區,否則如果所有執行緒共用一個棧的話,會引起呼叫棧混亂,並且因為CPU是以pcb來排程的,因此CPU排程的基本單位,所以每一個執行緒也都應該有自己的上下文資料來儲存CPU排程切換時的資料。(而這些都是線上程地址空間中,每一個執行緒都有自己的執行緒地址空間,它們相對來說獨立,但是執行緒地址空間是在虛擬地址空間內的)

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

void* start(void *arg){
    int num = (int)arg;
    while(1){
    printf("The pthread%d\n",num);
    sleep(1);
    }
}
int main()
{
    pthread_t tid;
    pthread_create(&tid,NULL,start,(void*)999);
    //pthread_t pthread_self(void);
    //  獲取呼叫執行緒的執行緒id(使用者態執行緒id)tid是pthread_t型別即無符號長整型lu
    printf("Main pthread ID:%lu\n",pthread_self());
    printf("The pthread!!ID:%lu\n",tid);
    while(1){
        printf("This is main pthread!!\n");
        sleep(1);
    }
    return 0;
}

在這裡插入圖片描述
Linux下執行緒操作函式都是庫函式,需要連結動態庫-pthread

thread: 用於接受一個使用者執行緒ID
attr: 使用者設定執行緒屬性,一般置空
start_routine:執行緒的入口函式,執行緒執行的就是這個函式,這個函式退出了,執行緒也就退出了
arg:用於給執行緒入口函式傳遞引數
返回值:成功:0 失敗:errno

其實在每一個task_struct裡邊都有pid、tgid,其中pid執行緒標識,tgid是描述該執行緒屬於哪個執行緒組(程序)的執行緒,這個tgid實際上是該執行緒所處執行緒組的首執行緒的pid,也就是說在一個執行緒組中tgid等於pid的那個執行緒就是主執行緒。

  1. 檢視程序中所有執行緒資訊1:ps -efL
ps -efL |head -1&&ps -efL |grep create

在這裡插入圖片描述
LWP:輕量級程序,這一列記錄的就是每個執行緒task_struct中的tid
NLWP:這個程序中有幾個執行緒
PPID:父程序ID
PID:程序ID(執行緒組tgid),也就是主執行緒的tid。
2、檢視執行緒:

ps -aL |head -1&&ps -aL |grep create

在這裡插入圖片描述

PID:程序ID(執行緒組tgid),也就是主執行緒的tid。

LWP:執行緒ID,各個task_struct中的tid.

執行緒沒有父子之分,所有執行緒都是有平級的,如果非要說有區別的話,那麼就是主執行緒和其他執行緒的區別。

執行緒終止

執行緒的退出方式:

  1. return num;退出:在main函式中呼叫return,效果是退出程序,線上程中呼叫return,退出執行緒。
  2. exit(num)針對整個程序,即使在非主執行緒中使用exit(),也會是整個程序退出.
void pthread_exit(void *retval);

       Compile and link with -pthread.

DESCRIPTION
The  pthread_exit()  function terminates the calling thread and returns a value
 via retval that (if the thread is joinable)is available to another thread in
  the same process that calls pthread_join(3).

retval儲存執行緒退出狀態資訊,如果不關心,可以置空。
終止一個呼叫執行緒,執行緒呼叫執行緒退出,main函式呼叫,主執行緒退出,其他執行緒成了殭屍執行緒,程序顯示殭屍程序,程序顯示的是主執行緒stat。終止一個呼叫執行緒,執行緒呼叫執行緒退出,main函式呼叫,主執行緒退出,其他執行緒成了殭屍執行緒,程序顯示殭屍程序,程序顯示的是主執行緒stat。

檢視執行緒的命令

 ps aux -L | head -1 && ps aux -L | grep exit | grep -v 'test'
ps -aL
int pthread_cancel(pthread_t thread);
	  Compile and link with -pthread.

主執行緒用來取消主執行緒自己的執行緒id為thread的執行緒

//這段程式碼用於演示執行緒退出的幾種方式
#include<stdio.h>
#include<unistd.h>
#include<pthread.h>
#include<stdlib.h>

void *thr_start(void *arg)
{
    //線上程中呼叫exit函式會怎樣?
    //程序要是退出了,那麼程序中的執行緒也會退出
    //exit(0);
    //return NULL;
    //sleep(5);
    //pthread_exit(NULL);
    while(1){
    printf("child pthread!!!\n");
    sleep(1);
    }
return NULL;
}
int main()
{
    pthread_t tid;
    int ret = -1;

    ret = pthread_create(&tid,NULL,thr_start,NULL);
    if(ret != 0){
        printf("pthread create error\n");
        return -1;
    }
    //int pthread_cancel(pthread_t thread);
    //取消普通執行緒
    pthread_cancel(tid);
    while(1){
        printf("first pthread!!\n");
        sleep(1);
    }
    return 0;
}

執行緒等待(執行緒分離)

主執行緒退出,其他執行緒也會形成殭屍執行緒:佔用了一部分資源不釋放,最終造成資源洩露。
執行緒等待就是接受執行緒的返回值,然後釋放執行緒的所有資源。
執行緒處於joinable狀態才能被等待,如果一個執行緒在呼叫pthread_join函式之前已經退出,則pthread_join函式立即返回,否則阻塞等待,直到這個指定的執行緒退出,才會返回。

pthread_join函式的返回值只需要考慮執行緒的退出碼或者errorno,不需要考慮執行緒異常的情況,因為一旦執行緒產生異常,系統會認為整個程序異常,這個程序就掛了。
 int pthread_join(pthread_t thread, void **retval);   //避免資源洩露,執行緒屬性是joinable狀態

pthread_t thread:指定等待執行緒的執行緒ID
void **retval:將執行緒退出資訊接收到retval中

The pthread_join() function waits for the thread specified by thread to terminate. If that thread has already terminated,
then pthread_join() returns immediately. The thread specified by thread must be joinable.

//演示執行緒退出等待,獲取執行緒返回值
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <pthread.h>

//執行緒需要被等待的條件是:執行緒處於joinable狀態,一個執行緒創建出來預設屬性就是joinable狀態
void* Func()
{
    sleep(2);//讓這個非主執行緒睡覺,讓主執行緒等它睡醒後,接收該執行緒的退出狀態值,釋放資源
    printf("I am not the main pthread\n");
    pthread_exit("I am not the main pthread !I will exit!");
    return "I am the best!!";
}

int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid,NULL,Func,NULL);
    if(ret!=0){
        perror("pthread_create error");
        exit(-1);
    }
   
    printf("I am the Main pthread\n");

    char* addr;//定義一級指標用於給pthread_join的第二個引數初始化,否則二級指標為空,不能使用
    char** ptr = &addr;
    sleep(5);
    pthread_join(tid,(void**)ptr);//主執行緒到這裡會阻塞等待它建立的執行緒id是tid的執行緒
    printf("%s\n",*ptr);

    //pthread_cancel(tid);
    //sleep(5);
    //pthread_join(tid,(void**)ptr);//主執行緒到這裡會阻塞等待它建立的執行緒id是tid的執行緒
    //如果一個執行緒是被取消的那麼它的返回值只有一個-1,PTHREAD_CANCELD
    //printf("%d\n",addr);
    return 0;
}

在這裡插入圖片描述
執行緒分離:功能是設定狀態

我們等待一個執行緒是因為需要獲取執行緒的返回值,並且釋放資源。那麼假如我不關心返回值,那麼這個等待將毫無意義,僅僅是為了釋放資源。因此就有一個執行緒屬性叫:執行緒分離屬性 detach屬性,這個屬性就是需要設定,它是告訴作業系統,這個指定的執行緒我不關心返回值,所以如果執行緒退出了,作業系統就自動把所有資源回收。而設定一個執行緒分離屬性我們常稱為執行緒分離

執行緒的detach屬性與joinable屬性相對應,也相沖突,兩者不會同時存在。如果一個執行緒屬性是detach,那麼pthread_join的時候將直接報錯,所以我們說,只有一個執行緒處於joinable狀態才可以被等待

執行緒被設定成detach屬性,退出後將自動釋放資源,不會形成殭屍執行緒
detach與joinable屬性相沖突,無法同時存在,設定detach就表明了不關心返回值

int pthread_detach(pthread_t thread);
執行緒也可以分離自己pthread_detach(pthread_self());

- 使用gdb除錯的注意事項

1、編譯過程一定要加-g選項:因為在Linux系統下,預設生成的是release(不加除錯資訊)版本的可執行程式,如果不加-g,則不能除錯。例如編譯hello.c生成hello的debug版本;

gcc -g hello.c -o hello

2、在開啟gdb除錯不想看到那麼一大堆版本資訊可以加-q,例如除錯hello

gdb -q hello

3、常用選項:

run/r:執行程式到結束 continue:從當前位置開始連續而非單步執行程式到結束
breaktrace(或bt):檢視各級函式調⽤及引數
start:開始單步除錯,next/n下一步
step/s:進入函式,類似於VS裡的F11
finish:執⾏到當前函式返回,然後挺下來等待命令

在這裡插入圖片描述

break/b:打斷點,可以加行號或者函式

在這裡插入圖片描述

info break/i b:檢視斷點資訊
info local:檢視當前棧幀區域性變數的值
delete/d breakpoints/number:刪除所有斷點/刪除斷點編號為number的斷點
print/p:打印表達式的值,通過表示式可以修改變數的值或者調⽤函式
p 變數:列印變數值。
q/ctrl+d:退出gdb