多線程(線程概念、線程控制、線程安全、信號量、線程池)

=====================================================================================

一、線程概念

功能:進行多任務處理
一個進程能夠完成任務的處理,通過pcb的描述,完成對程序的調度處理。

多任務處理:

  • 多創建幾個進程,一個進程就有一個pcb,能夠串行化的完成一個任務;
  • 在一個進程中多創建幾個pcb,因爲pcb是調度程序運行的描述,因此有多少個pcb就有多少個執行流程。

在進程中,我們說進程就是一個pcb,是程序動態運行的描述,通過pcb可以實現操作系統對程序運行的調度管理。現在學習的多線程,我們說線程是進程中的一條執行流,這個執行流在Linux下是通過pcb實現的,因此實際上Linux下的線程就是一個pcb,然而pcb是進程,並且Linux下的pcb共用一個虛擬地址空間,相較於傳統pcb更加輕量化,因此也被稱爲輕量級進程

示例:
在這裏插入圖片描述

多進程

在這裏插入圖片描述

多線程

在這裏插入圖片描述
== Linux下的進程其實是一個線程組,一個進程中可以有多個線程(多個pcb),編程是進程中的一條執行流。(進程(一個工廠),線程(工廠中幹活的工人),在Linux下幹活的就是pcb)。==

進程:是一個程序動態的運行,其實就是一個程序運行的描述 - - - pcb。
線程:是進程中的一條執行流,執行一個程序中的某段代碼。

在Linux下pcb可以實現程序的調度運行,因此在實現線程的時候,使用了pcb來實現;創建線程會伴隨在內核中創建一個pcb來實現程序的調度,作爲進程中的一條執行流。進程就是多個線程的一個合集,並且這個進程中的所有pcb共用進程中的大部分資源(程序運行時,操作系統爲程序運行所分配的所有資源),因此這些pcb在Linux下又稱爲輕量級進程。

根據不同學習階段,對pcb有不同的理解

  • 第一階段:pcb是進程,用於調度一個程序運行;
  • 第二階段:pcb是線程,是輕量級進程(爲了跟印象中的傳統進程進行區分),因爲線程是運行中程序的一條執行流,Linux下通過pcb實現這個執行流,並且共用同一份運行資源。

進程是操作系統資源分配的基本單位;(操作系統會爲一個程序的的運行分配所需的所有資源)
線程是cpu調度的基本單位

線程之間的獨有與共享

  • 獨有:標識符、寄存器、棧、信號屏蔽字、errno(系統調用完畢後會重置一個全局變量)、優先級。
  • 共享:虛擬地址空間(代碼段/數據段)、文件描述符表、信號處理的回調函數、用戶ID/組ID/工作路徑。

多線程/多進程進行多任務處理的優缺點分析

多線程的優點:
  • 線程間通信更加靈活方便;(除了進程間通信方式之外還有全局變量以及函數傳參 - - - 共有同一個虛擬地址空間,只要知道地址就能訪問同一塊空間);
  • 線程的創建和銷燬成本更低(創建線程創建一個pcb,共用的數據只需要使用一個指針指向同一處就可以了);
  • 同一個進程中的線程間調度成本更低(調度切換需要切換頁表);
多進程的優點:

適用於對於主程序安全性要求很高的場景:shell/網絡服務器

  • 多進程的健壯性,穩定性更高(異常以及一些系統調用(如:exit)直接針對整個進程生效);
共同的優點:
  • IO密集型程序:多任務並行處理(多磁盤可以實現同時處理);
    (IO密集型:程序中大量進行IO操作,對cpu要求並不高,因此執行流個數沒有太大要求);
  • CPU密集型程序:程序中進行大量的數據運算處理;cpu資源足夠,就可以同時處理,提高效率(通常執行流的個數是cpu核心數+1),創建線程很多的話,而cpu資源不夠多,會造成進程切換調度成本提高。

二、線程控制

通過代碼實現線程的創建 / 退出 / 等待 / 分離
進行線程控制的接口代碼,其實都是庫函數,也就是說,操作系統其實並沒有提供創建線程的接口,因此人們在用戶態使用函數封裝了一套線程庫,這套封裝的線程庫函數,提供了線程的各種操作。
使用庫函數創建一個線程(用戶態線程),本質上是在內核中創建一個輕量級進程來實現程序的調度。

線程創建

int pthread_create (pthread_t *thread, const pthread_attr_t *arr, void *( *start_routine)(void * ), void *arge);

thread:輸出型參數,用於獲取線程id(線程的操作句柄);
attr:線程屬性,用於在創建線程的同時設置線程屬性,通常置NULL;
start_routine:函數指針,這是一個線程的入口函數(線程運行的就是這個函數,函數運行完畢,則線程就會退出);
arg:通過線程入口函數,傳遞給線程的參數;
返回值:成功返回0,失敗返回非0值(錯誤編號);

tid是一個無符號長整型數據。一個線程有一個pcb,每個pcb都有一個pid(是一個整型數據)。

tid和pid有什麼聯繫?
在這裏插入圖片描述
tid是一個用戶態線程的id,線程的操作句柄,這個id其實就是線程獨有的這塊空間的首地址。
每個線程被創建出來後,都會開闢一塊空間,存儲自己的棧,自己的描述信息
pid是一個輕量級進程id,是內核中task_struct結構體中的id;
task_struct ->pid:輕量級進程id,也就是ps -efL看到的LWP;
task_struct ->tgid:線程組id,等於主線程id (也就是外邊所看到的進程id);

線程的操作大都是通過tid完成的

#include <stdio.h>
#include <unistd.h>  //sleep頭文件
#include <string.h>  //字符串操作頭文件
#include <pthread.h> //線程庫接口頭文件

int a = 100;

void *thr_start(void *arg)
{
    while(1) {
        printf("i am thread~~%s ---- %d\n", (char*)arg, a);
        sleep(1);
    }
    return NULL;
}
int main()
{
    pthread_t tid;
    char ptr[1024] = "chilema~~?";
    //pthread_create(獲取線程id, 線程屬性, 線程入口函數, 參數)
    int ret = pthread_create(&tid, NULL, thr_start, (void*)ptr);
    if (ret != 0) {
        printf("create thread failed!!\n");
        return -1;
    }
    printf("create thread success!! normal thread tid:%lu\n", tid);
    while(1) {
        printf("leihoua~~ ----%d\n", a);
        sleep(1);
    }
    return 0;
}

線程終止

如何退出一個線程

  • 線程入口函數運行完畢,線程就會自動退出- - - -在線程入口函數中調用return(但是main函數中return,退出的是進程而不是主線程);
  • void pthread_exit(void *retval); 退出線程接口,誰調用誰退出,retval是退出返回值;(exit無論在哪個進程調用,都是退出整個進程);
  • 主線程退出,並不會導致進程退出,只有所有的線程都退出了,進程纔會退出;
  • int pthread_cancel(pthread_t thread); 終止一個線程;退出的線程是被動取消的;

線程等待

等待一個線程的退出,獲取退出線程的返回值,回收線程所佔的資源。

  • 線程有一個屬性,默認創建出來是 joinable,處於這個屬性的線程,退出後,需要被其它線程等待獲取返回值回收資源。
  • int pthread_join(pthread_t thread, void *retval); - - - 等待指定線程退出,獲取其返回值;
    thread:需要等待退出的線程tid; – -- - -阻塞函數,線程沒有退出則一直等待;
    retval:輸出型參數,用於返回線程的返回值;
    線程的返回值是一個void
    ,是一個一級指針,若要通過一個函數的參數獲取一級指針,就要傳入一個一級指針變量的地址進來。

默認情況下,一個線程必須被等待,若不等待,則會造成資源泄露。

線程分離

將線程 joinable 屬性修改成 detach 屬性

  • 若是joinable 那麼就必須被等待;
  • 若是detach 那麼這個線程退出後則自動釋放資源,不需要被等待(因爲資源已經自動被釋放了);

分離一個線程,一定是對線程的返回值不感興趣,根本就不想獲取,但又不想一直等待線程退出,這種情況纔會分離線程。

int pthread_detach(pthread_t thread); - - - 將制定的線程分離出去(屬性改爲detach)

pthread_t pthread_self( void ); - - - 返回調用線程的id

  • pthread_exit是誰調用誰退出— 線程主動退出;
  • pthread_cancel 取消其它線程 —線程是被動退出;
  • tid通常創建這個線程的時候保存起來,後邊就可以通過tid對這個線程進行操作;
  • 分離線程,只是說線程退出後自動釋放資源;
#include <stdio.h>
#include  <unistd.h>
#include <stdlib.h>
#include <pthread.h>

void function()
{
    //char ptr[] = "這是我的返回值";//ptr有一塊空間在棧中,將字符串的值賦值進去
    char *ptr = "這是我的返回值"; //字符串在常量區,ptr只是保存了這個常量區的地址
    pthread_exit((void*)ptr);//在任意位置調用,都可以退出調用線程
}
void *thr_start(void *arg)
{
    //pthread_self 返回調用線程的tid
    //pthread_detach(pthread_t tid);
    pthread_t tid = pthread_self();
    pthread_detach(tid);//自己分離自己---實際上就是設置個屬性而已
    while(1) {
        printf("i am normal thread\n");
        sleep(5);
        function();
    }
    return NULL;
}
int main()
{
    pthread_t tid;
    int ret = pthread_create(&tid, NULL, thr_start, NULL);
    if (ret != 0) {
        printf("create thread failed!!\n");
        return -1;
    }
    //char *ptr;
    //pthread_join(tid, (void**)&ptr);
    //printf("retval:%s\n", ptr);
    //pthread_cancel(tid);
    while(1) {
        printf("i am main thread\n");
        sleep(1);
    }
    return 0;
}

三、線程安全

多個執行流對臨界資源爭搶訪問,但是不會出現數據二義性。

線程安全實現

同步:通過條件判斷保證對臨界資源訪問的合理性;
互斥:通過同一時間對臨界資源訪問的唯一性實現臨界資源訪問的安全性;

互斥的實現:互斥鎖

互斥鎖實現互斥原理:互斥鎖本身是一個只有0/1的計數器,描述了一個臨界資源當前的訪問狀態,所有執行流在訪問臨界資源時都需要先判斷當前的臨界資源狀態是否允許訪問,如果不允許則讓執行流等待,否則可以讓執行流訪問臨界資源,但是在訪問期間需要將狀態修改爲不可訪問狀態,這期間不允許其他執行流進行訪問。

互斥鎖具體操作流程和接口介紹

  • 定義互斥鎖變量 pthread_mutex_t mutex;
  • 初始化互斥鎖變量
    pthread_mutex_init(pthread_mutex_t *mutex, pthread_mutexattr_t *atr);
    pthread_mutex_t mutex = PTHREAD_MUTEX_INITALIZER;
  • 在訪問臨界資源之前進行加鎖操作(不能加鎖則等待,可以加鎖則修改資源狀態,然後調用返回,訪問臨界資源)
    pthread_mutex_lock(pthread_mutex_t *mutex); (阻塞加鎖,如果當前不能加鎖(鎖已經被別人加了),則一直等待直到加鎖成功調用返回);
    /pthread_mutex_trylock(pthread_mutex_t *mutex);(非阻塞加鎖,如果當前不能加鎖,則立即報錯返回 - - - - EBUSY)
    掛起等待:將線程狀態置爲可中斷休眠狀態(表示當前休眠);
    被喚醒:將線程狀態置爲運行狀態;
  • 在臨界資源訪問完畢之後進行解鎖操作(將資源狀態置爲可訪問,將其他執行流喚醒);
    pthread_mutex_unlock(pthread_mutex_t *mutex);
  • 銷燬互斥鎖
    pthread_mutex_destroy(pthread_mutex_t *mutex);

所有的執行流都需要通過同一個互斥鎖實現互斥,意味着互斥鎖本身就是一個臨界資源,大家都會訪問。

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

int ticket = 100;
pthread_mutex_t mutex;

void *thr_scalpers(void *arg)
{
    while(1) {
        //加鎖一定是隻保護臨界資源的訪問
        pthread_mutex_lock(&mutex);
        if (ticket > 0) {
            //有票就一直搶
            usleep(1000);
            printf("%p-I got a ticket:%d\n", pthread_self(), ticket);
            ticket--;
            pthread_mutex_unlock(&mutex);
        }else {
            //加鎖後在任意有可能退出線程的地方都要解鎖
            pthread_mutex_unlock(&mutex);
            pthread_exit(NULL);
        }
    }
    return NULL;
}

int main()
{
    pthread_t tid[4];

    int i, ret;
    //互斥鎖的初始化一定要放在線程創建之前
    pthread_mutex_init(&mutex, NULL);
    for (i = 0; i< 4; i++) {
        ret = pthread_create(&tid[i], NULL, thr_scalpers, NULL);
        if (ret != 0) {
            printf("thread create error");
            return -1;
        }
    }
    for (i = 0; i < 4; i++) {
        pthread_join(tid[i], NULL);
    }
    //互斥鎖的銷燬一定是不再使用這個互斥鎖
    pthread_mutex_destroy(&mutex);
    return 0;
}

互斥鎖本身的操作首先必須是安全的,互斥鎖自身計數的操作是原子操作。

在這裏插入圖片描述
不管當前mutex的狀態是什麼,反正一步交換後,其他的線程都是不可訪問的;這時候當前進程就可以慢慢判斷了;

死鎖

多個執行流對鎖資源進行爭搶訪問,但是因爲訪問推進順序不當,造成互相等待最終導致程序流程無法繼續推進,這時候就造成了死鎖,(死鎖實際上就是一種程序流程無法繼續推進,卡在某個位置)。

死鎖產生的必要條件(有一條不具備就不會產生死鎖)

  1. 互斥條件:我加了鎖,別人就不能再繼續加鎖;
  2. 不可剝奪條件:我加了鎖,別人不能解我的鎖,只有我能解鎖;
  3. 請求與保持條件:我加了A鎖,然後去請求B鎖;如果不能對B鎖加鎖,則也不釋放A鎖;
  4. 環路等待條件:我加了A鎖,然後去請求B鎖;另一個人也加了B鎖,然後去請求A鎖;

死鎖的預防:破壞死鎖產生的必要條件(主要避免3和4兩個條件的產生)。
死鎖的避免:死鎖檢測算法 / 銀行家算法

銀行家算法的思路:系統的安全狀態/非安全狀態
一張表記錄當前有哪些鎖,一張表記錄已經給誰分配了哪些鎖,一張表記錄誰當前需要哪些鎖
按照三張表進行判斷,判斷若給一個執行流分配了指定的鎖,是否會達成環路等待條件,導致系統的運行進入不安全狀態,如果有可能就不能分配。反之,若分配了之後不會造成環路等待,系統是安全的,則分配這個鎖,(破壞環路等待條件)。
後續若不能分配鎖,可以資源回溯,把當前執行流中已經加了的鎖釋放掉,(破壞請求與保持)。

死鎖是如何產生的?如何預防和避免?
加鎖對臨界資源進行保護,實際上對程序的性能時一個極大的挑戰。在高性能程序中會將就一種無鎖編程(CAS鎖 / 一對一的阻塞隊列 / atomic原子操作)。

同步的實現:通過條件判斷實現臨界資源訪問的合理性(條件變量);

  • 當前是否滿足獲取資源的條件,若不滿足,則讓執行流等待,等到滿足條件能夠獲取的時候再喚醒執行流。
  • 條件變量實現同步:只提供了兩個功能接口:讓執行流等待的接口和喚醒執行流的接口。因此條件的判斷是需要進程自身進行操作,自身判斷是否滿足條件,不滿足的時候調用條件變量接口使線程等待。

使用接口介紹

  • 定義條件變量: pthread_cond_t cond;
  • 初始化條件變量:
    pthread_cond_init( pthread_cond_t *cond, pthread_condattr_t *attr);
    pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
  • 是線程掛起休眠,若資源獲取條件不滿足時調用接口進行阻塞等待
    pthread_cond_wait( pthread_cond_t *cond, pthread_mutex_t *mutex); 一直死等別人的喚醒條件變量是搭配互斥鎖一起使用的。
    pthread_cond_timedwait( pthread_cond_t * ,pthread_mutex_t *,struct timespec *); 設置阻塞超時時間的等待接口,(等待指定時間內都沒有被喚醒則自動醒來)
  • 喚醒線程的接口
    pthread_cond_signal(pthread_cond_t *); - - -喚醒至少一個等待的進程(並不是喚醒單個)
    pthread_cond_broadcast(pthread_cond_t *); - - -喚醒所有等待的進程
  • 銷燬條件變量
    pthread_cond_destroy(pthread_cond_t *);

例子:
在這裏插入圖片描述
在這裏插入圖片描述

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

int bowl = 0;//默認0表示碗中沒有飯


pthread_cond_t cook_cond;   // 實現線程間對bowl變量訪問的同步操作
pthread_cond_t customer_cond;   // 實現線程間對bowl變量訪問的同步操作
pthread_mutex_t mutex; // 保護bowl變量的訪問操作

void *thr_cook(void *arg)
{
    while(1) {
        //加鎖
        pthread_mutex_lock(&mutex);
        while (bowl != 0){//表示有飯,不滿足做飯的條件
            //讓廚師線程等待,等待之前先解鎖,被喚醒之後再加鎖
            //pthread_cond_wait接口中就完成了解鎖,休眠,被喚醒後加鎖三部操作
            //並且解鎖和休眠操作是一步完成,保證原子操作
            pthread_cond_wait(&cook_cond, &mutex);
        }
        bowl = 1; //能夠走下來表示沒飯,bowl==0, 則做一碗飯,將bowl修改爲1
        printf("I made a bowl of rice\n");
        //喚醒顧客喫飯
        pthread_cond_signal(&customer_cond);
        //解鎖
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}
void *thr_customer(void *arg)
{
    while(1) {
        //加鎖
        pthread_mutex_lock(&mutex);
        while (bowl != 1) { // 沒有飯,不滿足喫飯條件,則等待
            //沒有飯則等待,等待前先解鎖,被喚醒後加鎖
            pthread_cond_wait(&customer_cond, &mutex);
        }
        bowl = 0; // 能夠走下來表示有飯 bowl==1, 喫完飯,將bowl修改爲0
        printf("I had a bowl of rice. It was delicious\n");
        //喚醒廚師做飯
        pthread_cond_signal(&cook_cond);
        //解鎖
        pthread_mutex_unlock(&mutex);
    }
    return NULL;
}
int main()
{
    pthread_t cook_tid[4], customer_tid[4];
    int ret, i;

    pthread_mutex_init(&mutex, NULL);
    pthread_cond_init(&cook_cond, NULL);
    pthread_cond_init(&customer_cond, NULL);

    for (i = 0; i < 4; i++) {
        ret = pthread_create(&cook_tid[i], NULL, thr_cook, NULL);
        if (ret != 0) {
            printf("pthread_create error\n");
            return -1;
        }
        ret = pthread_create(&customer_tid[i], NULL, thr_customer, NULL);
        if (ret != 0) {
            printf("pthread_create error\n");
            return -1;
        }
    }

    pthread_join(cook_tid[0], NULL);
    pthread_join(customer_tid[0], NULL);

    pthread_mutex_destroy(&mutex);
    pthread_cond_destroy(&cook_cond);
    pthread_cond_destroy(&customer_cond);
    return 0;
}
生產者與消費者模型

優點:解耦合、支持忙閒不均、支持開發
實現:一個場所(線程安全的緩衝區- - - 數據隊列),兩種角色(生產者和消費者),三種關係(實現線程安全)。
在這裏插入圖片描述

#include <cstdio>
#include <iostream>
#include <vector>
#include <pthread.h>
#include <semaphore.h>

#define QUEUE_MAX 5
class RingQueue{
    public:
        RingQueue(int maxq = QUEUE_MAX):_queue(maxq), _capacity(maxq),
        _step_read(0), _step_write(0){
            //sem_init(信號量, 進程/線程標誌, 信號量初值)
            sem_init(&_lock, 0, 1);//用於實現互斥鎖
            sem_init(&_sem_data, 0, 0);//數據空間計數初始爲0
            sem_init(&_sem_idle, 0, maxq);//空閒空間計數初始爲節點個數
        }
        ~RingQueue(){
            sem_destroy(&_lock);
            sem_destroy(&_sem_data);
            sem_destroy(&_sem_idle);
        }
        bool Push(int data){
            //1. 判斷是否能夠訪問資源,不能訪問則阻塞
            sem_wait(&_sem_idle);//-空閒空間計數的判斷,空閒空間計數-1
            //2. 能訪問,則加鎖,保護訪問過程
            sem_wait(&_lock);//lock計數不大於1,當前若可以訪問則-1,別人就不能訪問了
            //3. 資源的訪問
            _queue[_step_write] = data;
            _step_write = (_step_write + 1) % _capacity;//走到最後,從頭開始
            //4. 解鎖
            sem_post(&_lock);//lock計數+1,喚醒其它因爲加鎖阻塞的線程
            //5. 入隊數據之後,數據空間計數+1,喚醒消費者
            sem_post(&_sem_data);
            return true;
        }
        bool Pop(int *data){
            sem_wait(&_sem_data);//有沒有數據
            sem_wait(&_lock); // 有數據則加鎖保護訪問數據的過程
            *data = _queue[_step_read]; //獲取數據
            _step_read = (_step_read + 1) % _capacity;
            sem_post(&_lock); // 解鎖操作
            sem_post(&_sem_idle);//取出數據,則空閒空間計數+1,喚醒生產者
            return true;
        }
    private:
    std::vector<int> _queue; // 數組, vector需要初始化節點數量
    int _capacity; // 這是隊列的容量
    int _step_read; // 獲取數據的位置下標
    int _step_write;//寫入數據的位置下標
    //這個信號量用於實現互斥
    sem_t _lock ;
    //這個信號量用於對空閒空間進行計數
    //---對於生產者來空閒空間計數>0的時候才能寫數據 --- 初始爲節點個數
    sem_t _sem_idle; 
    // 這個信號量用於對具有數據的空間進行計數
    // ---對於消費者來說有數據的空間計數>0的時候才能取出數據 -- 初始爲0
    sem_t _sem_data; 
};


void *thr_productor(void *arg) 
{
    //這個參數是我們的主線程傳遞過來的數據
    RingQueue *queue = (RingQueue*)arg;//類型強轉
    int i = 0;
    while(1) {
        //生產者不斷生產數據
        queue->Push(i);//通過Push接口操作queue中的成員變量
        printf("productor push data:%d\n", i++);
    }
    return NULL;
}
void *thr_customer(void *arg) 
{
    RingQueue *queue = (RingQueue*)arg;
    while(1) {
        //消費者不斷獲取數據進行處理
        int data;
        queue->Pop(&data);
        printf("customer pop data:%d\n", data);
    }
    return NULL;
}
int main()
{
    int ret, i;
    pthread_t ptid[4], ctid[4];
    RingQueue queue;

    for (i = 0; i < 4; i++) {
        ret = pthread_create(&ptid[i], NULL, thr_productor, (void*)&queue);
        if (ret != 0) {
            printf("create productor thread error\n");
            return -1;
        }
        ret = pthread_create(&ctid[i], NULL, thr_customer, (void*)&queue);
        if (ret != 0) {
            printf("create productor thread error\n");
            return -1;
        }
    }
    for (i = 0; i < 4; i++) {
        pthread_join(ptid[i], NULL);
        pthread_join(ctid[i], NULL);
    }
    return 0;
}

多線程的併發- - -操作系統層面的輪詢調度(或者CPU資源足夠情況下的並行)

生產者與消費者,其實是兩種業務處理的線程而已,我們創建線程就可以,實現的關鍵在於線程安全隊列。
封裝一個線程安全的BlockQueue- - -阻塞隊列- - -向外提供線程安全的入隊/出隊操作

class BlockQueue
{
public:
	BlockQueue();  //編碼風格:純輸入參數 -const& /輸出型參數 指針/輸出入輸出型 &(引用)
	bool Push(const int &data);  //入隊數據
	bool Pop(int *data);         //出隊數據
private:
	std::queue<int> _queue;     //STL中queue容器,是非線程安全的---因爲STL設計之初就是奔着性能去的,並且功能多了耦合度就高了
	int _capacity;             //隊列中節點的最大數量---數據也不能無限制添加,內存耗盡程序就崩潰了
	pthread_mutex_t_mutex;
	pthread_cond_t_productor_cond; //生產者隊列
	pthread_cond_t_customer_cond; //消費者隊列
}

注意事項

1、條件變量需要搭配互斥鎖一起使用,prhread_cond_wait 集合瞭解鎖/休眠/被喚醒後加鎖的散步操作;
2、程序員在程序中對訪問條件是否滿足的判斷需要使用while 循環進行判斷;
3、在同步實現時,多種不同的角色線程需要使用多個條件變量,不要讓所有的線程等待在一個條件變量上。

在這裏插入圖片描述

四、信號量

可以用於實現進程 / 線程間同步與互斥,信號本質就是一個計數器 + pcb等待隊列
同步的實現:通過自身的計數器對資源進行計數,並且通過計數器的資源計數,判斷進程/線程是否能夠符合訪問資源的條件,若符合則可以訪問,若不符合則調用提供的接口使進程/線程阻塞;其他進程/線程促使條件滿足後,可以喚醒pcb等待隊列上的pcb。

互斥的實現:保證計數器的技術不大於1,就保證了資源只有一個,同一時間只有一個進程/線程能夠訪問資源,實現互斥。

代碼的操作

  • 1、定義信號量:sem_t
  • 2、初始化信號量:int sem_init(sem_t *sem, int pshared, unsigned int value);
    sem:定義的信號量變量;
    pshared:0 - - -用於線程間; 非0 - - - 用於進程間;
    value:初始化信號量的初值 - - - 初始資源數量有多少計數就是多少;
    返回值:成功則返回0,失敗返回 -1;
  • 3、在黨文臨界資源之前,先訪問信號量,判斷是否能夠訪問(計數 -1)
    int sem_wait(sem_t *sem); - - - 通過自身計數判斷是否滿足訪問條件,不滿足則直接一直阻塞線程 / 進程;
    int sem_trywait(sem_t *sem); - - - 通過自身計數判斷是否滿足訪問條件,不滿足則立即報錯返回,WINAL;
    int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout); - - - 不滿足則等待指定時間,超時後報錯返回 - - -ETIMEDOUT
  • 4、促使訪問條件滿足 +1,喚醒阻塞線程 / 進程
    int sem_post(sem_t *sem); - - - 通過信號量喚醒自己阻塞隊列上的pcb;
  • 5、銷燬信號量
    int sem_destroy(sem_t *sem);

通過信號量實現一個生產者與消費者模型 - - -線程安全的阻塞隊列
使用數組實現一個隊列
隊列:滿足先進先出特性的容器都是隊列
在這裏插入圖片描述

class RingQueue
{
	std::vector<int>_queue;   //數組
	int _capcity;         //這是隊列的容量
	int _strp_read;        //獲取數據的位置下標
	int _step_write;       //寫入數據的位置下標
	
	sem_t lock;     //這個信號量用於實現互斥
	sem_t_sem_idle;   //這個信號量用於對空閒空間進行計數,對於生產者來空閒空間計數>0的時候才能寫數據(初始化爲節點個數)
	sem_t_sem_data;   //這個信號量用於對具體數據的空間進行計數,對於消費者來說有數據的空間計數 >0 的時候才能取出數據(初始值爲0)
}

五、線程池

線程池:線程的池子,有很多線程,但是數量不會超過池子的限制。需要用到多執行流進行任務處理的時候,就從池子中取出一個線程去處理。
應用場景:由大量的數據處理請求,需要多執行流併發/並行處理。

若是一個數據請求的到來伴隨着一個線程的創建去處理,則會產生一些風險以及一些不必要的損耗:

  • 1、線程若不限制數量的創建,在峯值壓力下,線程創建過多,資源耗盡,有程序崩潰的風險;
  • 2、處理一個任務的時間:創建線程 t1 + 任務處理事件 t2 + 線程銷燬時間 t3 = T,若 t2/T 比例佔據不夠高,則表示大量的資源用於線程的創建和銷燬成本上,因此線程池使用已經創建好的線程進行循環任務處理,就避免了大量線程的頻繁創建與銷燬的時間成本。

自主編寫一個線程池:大量線程(每個線程中都是進行循環任務處理)+ 任務緩衝隊列

線程的入口函數,都是在創建線程的時候,就固定傳入,導致線程池中的線程進行任務處理的方式過於單一
因爲線程的入口函數都是一樣的,處理流程也都是一樣的,只能處理單一方式的請求,靈活性太差

若任務隊列中的任務,不僅僅是單純的數據,而是包含任務處理方法在內的數據,這時候,線程池中的線程只需要使用傳入的方法,處理傳入的數據即可,不需要關心是什麼數據,如何處理,提高線程池的靈活性。

線程池就類似於一個實現了消費者業務的生產者與消費者模型

**每個線程的入口函數中,只需要不斷的獲取任務節點,調用任務節點中Run接口就可以實現處理了。

typedef void(*_handler)(int data);
class MyTask{
public:
	SteTask(int data, handler_t handler); //用戶自己傳入要處理的數據和方法,組織出一個任務節點
	Run(){ return_handler(_data);}
private:
	int _data;  //要處理的數據
	handler_t_handler;  //處理數據的方法
}
class ThreadPool{
	int thr_max; //定義線程池中線程的最大數量,初始化時創建相應數量的線程即可
	std::queue<MyTask>_queue; 
	pthread_mutex_t  _mutex; //實現_queue操作的安全性
	pthread_cond_t _cond; //實現線程池中消費者線程的同步
}

要處理什麼數據,什麼樣處理的方法,組織成一個任務節點,交給線程池,線程中找出任意一個線程只需要使用方法處理數據即可。

STL中的容器都是線程安全的嗎? - - - - - 不是
只能指針是線程安全的嗎? - - unique_ptr 因爲局部操作/ shared_ptr 原子操作,不涉及線程安全的問題

線程池.hpp 程序:

#include <cstdio>
#include <iostream>
#include <queue>
#include <stdlib.h>
#include <pthread.h>

typedef void (*handler_t)(int);
class ThreadTask
{
    public:
        ThreadTask(){
        }
        void SetTask(int data, handler_t handler) {
            _data = data;
            _handler = handler;
        }
        void Run() {//外部只需要調用Run,不需要關係任務如何處理
            return _handler(_data);
        }
    private:
        int _data;//任務中要處理的數據
        handler_t _handler;//任務中處理數據的方法
};

#define MAX_THREAD 5
class ThreadPool
{
    public:
        ThreadPool(int max_thr = MAX_THREAD):_thr_max(max_thr){
            pthread_mutex_init(&_mutex, NULL);
            pthread_cond_init(&_cond, NULL);
            for (int i = 0; i < _thr_max; i++) {
                pthread_t tid;
                int ret = pthread_create(&tid, NULL, thr_start, this);
                if (ret != 0) {
                    printf("thread create error\n");
                    exit(-1);
                }
            }
        }
        ~ThreadPool(){
            pthread_mutex_destroy(&_mutex);
            pthread_cond_destroy(&_cond);
        }
        bool TaskPush(ThreadTask &task) {
            pthread_mutex_lock(&_mutex);
            _queue.push(task);
            pthread_mutex_unlock(&_mutex);
            pthread_cond_broadcast(&_cond);//如對後喚醒所有線程,誰搶到誰處理
            return true;
        }
        // 類的成員函數,有一個隱藏的默認參數,是this指針
        // 線程入口函數,沒有this指針,如何操作私有成員呢??
        static void *thr_start(void *arg) {
            ThreadPool *p = (ThreadPool *) arg;
            //不斷的從任務隊列中取出任務,執行任務的Run接口就可以
            //每一個任務節點中包含了要處理的數據,以及如何處理的函數
            while(1) {
                pthread_mutex_lock(&p->_mutex);
                while(p->_queue.empty()) {
                    pthread_cond_wait(&p->_cond, &p->_mutex);
                }
                ThreadTask task;
                task = p->_queue.front();
                p->_queue.pop();
                pthread_mutex_unlock(&p->_mutex);
                task.Run();//任務的處理要放在解鎖之外,因爲當前的所保護的時隊列的操作
            }
            return NULL;
        }
    private:
        int _thr_max; // 線程池中線程的最大數量--根據這個初始化創建指定數量的線程
        std::queue<ThreadTask> _queue;
        pthread_mutex_t _mutex;//保護隊列操作的互斥鎖
        pthread_cond_t _cond;//實現從隊列中獲取節點的同步條件變量
};

線程池 main 程序:

#include <unistd.h>
#include "threadpool.hpp"

void test_func(int data)
{
    int sec = (data % 3) + 1;
    printf("tid:%p -- get data:%d , sleep:%d\n", pthread_self(), data, sec);
    sleep(sec);
}
void tmp_func(int data) {
    printf("tid:%p -- tmp_func\n", pthread_self());
    sleep(1);
}
int main()
{
    ThreadPool pool;
    for(int i = 0; i < 10; i++) {
        ThreadTask task;
        if (i % 2 == 0) {
            task.SetTask(i, test_func);
        }else {
            task.SetTask(i, tmp_func);
        }
        pool.TaskPush(task);
    }

    sleep(1000);
    return 0;
}

線程安全的單例模式

單例模式:是一種典型常用的一種設計模式,一份資源只能被申請加載一次 / 單例模式的方法創建的類在當前進程中只有一個實例。

實現方式

  • 餓漢方式:資源的程序初始化的時候就去加載,後面使用的話就直接使用。使用的時候比較流暢,有可能會加載用不上的資源,並且會導致程序初始化的時間比較慢。(使用static就可以將一個成員變量設置爲靜態變量,則所有對象共用一份資源,並且在程序初始化時就會申請資源,不涉及線程安全)。
  • 懶漢模式:資源在使用的時候發現還沒有加載,則申請加載。程序初始化比較快,第一次運行某個模塊的時候就會比較慢,因爲這時候去加載相應資源。

實現過程中需注意的細節

  • 使用 static,保證所有對象使用同一份資源;
  • 使用 volatile,放置編譯器過度優化;
  • 實現線程安全,保證資源判斷以及申請過程是安全的;
  • 外部二次判斷,以及避免資源已經加載成功每次獲取都要加鎖解鎖,帶來的鎖衝突;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章