多線程控制&多線程安全&死鎖&讀寫鎖

多線程概念

在傳統操作系統上pcb是一個進程,描述一個程序的運行,還有一個tcp描述實現線程,但是 在linux下使用pcb描述實現了程序調度並且這些pcb共用同一個虛擬地址空間,相較於傳統的pcb更加輕量化一點,因此也把linux下的pcb稱之爲輕量級進程。

進程是系統資源分配的基本單位。
線程是CPU調度的基本單位。

線程間的獨有與共享:

獨有:棧,寄存器,信號屏蔽字,errno,標識符
共享:虛擬地址空間(代碼段,數據段),文件描述表,信號處理方式,工作路徑,用戶ID,組ID

多進程/多線程進行多任務處理(優缺點):

多線程:線程間通信更加方便靈活(全局變量,函數傳參)
線程的創建/銷燬成本更低
線程間的調度成本更低
異常和某個系統調用針對的是整個進程。
多進程:具有獨立性,因此更加穩定,健壯。
例如:對主功能程序安全性穩定性要求更高的最好使用多進程(shell/服務器),剩下的使用多線程
共同優點
CPU密集型程序/IO密集型程序
並行壓縮CPU處理/並行壓縮IO等待時間
在CPU資源足夠的情況下,可以使程序的性能更高

線程控制

線程控制的接口都是庫函數(操作系統並沒有向用戶提供創建一個輕量級進程的接口,因此大佬門才封裝的一套線程控制接口,所以在使用的時候鏈接庫文件 -lpthread)

線程創建

int pthread_create(pthread_t tid,pthread_attr_t attr,void(thread_routine)(voidarg),void arg);**

tid:用於獲取線程id,通過這個id可以找到線程的描述信息,進而訪問pcb(輕量級進程完成控制)【線程在虛擬地址空間中開闢的線程空間的首地址(線程信息)】
attr:線程屬性,通常置NULL
thread_routine:線程入口函數,創建一個線程就是爲了運行這個函數,函數運行完畢,則線程退出
arg:通過線程入口函數傳遞給線程的參數
返回值:0-成功,非0值-失敗errno

ps -efL | grep create:查看線程
在這裏插入圖片描述
代碼演示:

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

void* thread_start(void* arg){

    while(1){
        printf("主線程傳遞了一個參數:%s\n",(char*)arg);
        sleep(1);
    }
    return NULL;
}
int main(){

    pthread_t tid;//無符號長整形
    char buf[]="好運來~\n";
    int ret=pthread_create(&tid,NULL,thread_start,(void*)buf);

    if(ret!=0){
        printf("thread create error:%d\n",ret);
        return -1;
    }
    while(1){
        printf("i am main thread\n");
        sleep(1);
    }
    return 0;
}

運行結果:
在這裏插入圖片描述

線程終止

1.普通線程入口函數中的return
注意:main函數中的return退出的是進程

2.void pthread_exit(viod retval);//退出一個線程,誰調用誰退出*

retval-線程返回值

代碼演示:

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

void* thread_start(void* arg){
   sleep(5);
   pthread_exit(NULL);
    while(1){
        printf("主線程傳遞了一個參數:%s\n",(char*)arg);
        sleep(1);
    }
    return NULL;
}
int main(){

    pthread_t tid;//無符號長整形
    char buf[]="好運來~\n";
    int ret=pthread_create(&tid,NULL,thread_start,(void*)buf);

    if(ret!=0){
        printf("thread create error:%d\n",ret);
        return -1;
    }
    while(1){
        printf("i am main thread\n");
        sleep(1);
    }
    return 0;
}

3.int pthread_cancel(pthread_t tid);//退出指定的線程

tid:就是指定的線程id
代碼演示:

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

void* thread_start(void* arg){
    while(1){
        printf("主線程傳遞了一個參數:%s\n",(char*)arg);
        sleep(1);
    }
    return NULL;
}
int main(){

    pthread_t tid;//無符號長整形
    char buf[]="好運來~\n";
    int ret=pthread_create(&tid,NULL,thread_start,(void*)buf);

    if(ret!=0){
        printf("thread create error:%d\n",ret);
        return -1;
    }
    
    sleep(3);
    pthread_cancel(tid);
    while(1){
        printf("i am main thread\n");
        sleep(1);
    }
    return 0;
}

注意

1.線程退出不會完全釋放資源,需要被其他線程等待。
2.取消自己是一種違規操作(主線程退出)。
3.主線程退出,並不影響整個進程的運行,只有所有的線程退出,進程纔會退出。

線程等待

等待一個線程的退出,獲取退出線程的返回值,回收這個線程所佔用的資源。
int pthread_join(pthread_t tid,void** retval);

tid:指定要等待的線程id
retval:用於獲取線程退出返回值

代碼演示:

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

void* thread_start(void* arg){

    char* buf="我是錦鯉\n";
    sleep(5);
    pthread_exit(buf);
    while(1){
        printf("主線程傳遞了一個參數:%s\n",(char*)arg);
        sleep(1);
    }
    return NULL;
}
int main(){

    pthread_t tid;//無符號長整形
    char buf[]="好運來~\n";
    int ret=pthread_create(&tid,NULL,thread_start,(void*)buf);

    if(ret!=0){
        printf("thread create error:%d\n",ret);
        return -1;
    }
    void* retval=NULL;
    pthread_join(tid,&retval);
    printf("retval:%s\n",retval);
    while(1){
        printf("i am main thread\n");
        sleep(1);
    }
    return 0;
}

運行結果:
在這裏插入圖片描述
注意:

不是所有的線程都能被等待,一個線程被創建默認情況下有一個屬性-joinable;處於joinable屬性的線程退出後,不會自動釋放資源,需要被等待。

線程分離

將線程的屬性從joinable設置爲detach;處於detach屬性的線程退出後會自動釋放資源,不需要被等待。

等待一個被分離的線程,則pthread_join會返回錯誤:這不是一個joinable線程(因爲在獲取返回值的時候將獲取不到,detach屬性的線程退出後已經自動釋放了資源)

int pthread_detach(pthread_t tid);//分離指定的線程
線程的分離可以在任意地方,可以在線程入口函數中讓線程分離自己,也可以讓創建線程在創建之後直接分離。

線程安全

在多個執行流中對同一個臨界資源進行訪問,而不會造成數據二義。

如何實現線程安全?

同步

通過一些條件判斷來實現多個執行流對臨界資源訪問的合理性(有資源則訪問,沒有資源則等着,等有資源了再被喚醒)。
條件變量:

1.向用戶提供兩個接口:使一個線程等待的接口和喚醒一個線程的接口
2.等待隊列
posix標準的信號量:

互斥

通過保證同一時間只有一個執行流可以對臨界資源進行訪問(一個執行流訪問期間,其他執行流不能訪問),來保證數據訪問的安全性。
互斥鎖:用於標記當前臨界資源的訪問狀態

計數器:0/1 0-表示不可訪問 1-表示可以訪問
每一個線程訪問臨界資源之前,先判斷計數,當前臨界資源的狀態(是否有人正在訪問–正在訪問的線程將狀態置爲了0(不可訪問狀態))
1.第一個線程訪問的時候,判斷可以訪問,因此將狀態置爲不可訪問,然後去訪問資源
2.其他線程訪問的時候,發現不可訪問,就陷入等待(可中斷休眠狀態)
3.第一個線程訪問臨界資源完畢後,將狀態置爲1(可以訪問)喚醒(將pcb狀態置爲運行態)等待線程,大家重新開始競爭這個資源

死鎖

多個執行流在對多個鎖資源進行爭搶操作,但是因爲推進順序不當,而導致互相等待,流程無法繼續推進的情況。
死鎖產生的四個必要條件:

1.互斥條件:一個鎖只有一個人能加,我加了鎖,其他人就不能加了
2.不可剝奪條件:我加的鎖,別人不能替我釋放
3.請求與保持條件:我加了A鎖,然後請求B鎖,但是請求不到B鎖,我也不釋放A鎖
4.環路等待條件:我拿着A鎖請求B鎖,對方拿着B鎖請求A鎖

預防死鎖:破壞產生死鎖的四個必要條件

1.鎖資源按順序一次性分配
2.加鎖的時候可以使用非阻塞加鎖,若無法加鎖,則將手中的其他鎖釋放

避免死鎖:

銀行家算法:
定義兩種狀態:安全/非安全-
現在有多少資源
現在那些人已經借了多少錢
當前還有那些人需要借多少錢
若給一個執行流分配指定的鎖有可能會造成環路等待(非安全狀態),則不予分配,並且回溯釋放當前執行流已有的資源

讀者寫者模型–讀寫鎖

少量寫臨界資源的執行流+大量讀臨界資源的執行流
特性:不能同時寫,但是可以同時讀(寫互斥,讀共享),寫的時候別人既不能寫,也不能讀;但是讀的時候大家可以一起讀,但是讀的時候不能寫

讀寫鎖的實現:

兩個計數器:讀者計數/寫者計數
讀者計數:>0,表示當前有人正在讀,想要加寫鎖的人就需要等待,而想要加讀鎖可以繼續加
寫者計數:>0,表示當前有人正在寫, 想要加寫鎖的人就需要等待,想要加讀鎖也需要等待
其中不能加鎖時的等待,通過自旋鎖實現
自旋鎖:循環判斷條件,並且強佔CPU(一直處於運行狀態,判斷條件,CPU不可剝奪),比較消耗CPU,比較適合於等待時間比較短的操作

發佈了84 篇原創文章 · 獲贊 72 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章