c/c++:線程同步(互斥鎖、死鎖、讀寫鎖、條件變量、生產者和消費者模型、信號量)


目錄

1. 概念

2. 互斥鎖

3. 死鎖

4. 讀寫鎖

5. 條件變量

5.1 生產者和消費者模型

6. 信號量

 
1. 概念

    線程同步:

 > 當有一個線程在對內存進行操作時,其他線程都不可以對這個內存地址進行操作,直到該線程完成操作。
  > - 在多個線程操作一塊共享數據的時候
  >   - 按照先后順序依次訪問
  >   - 有原來的 並行 -> 串行

    臨界資源:一次只允許一個線程使用的資源。
    原子操作:

  > 原子操作,就是說像原子一樣不可再細分不可被中途打斷。

  > 一個操作是原子操作,意思就是說這個操作是以原子的方式被執行,要一口氣執行完,執行過程不能夠被OS的其他行為打斷,是一個整體的過程,在其執行過程中,OS的其它行為是插不進來的。

 
2. 互斥鎖

    互斥鎖類型:

    // pthread_mutex_t 互斥鎖的類型
    pthread_mutex_t mutex;

    互斥鎖特點:讓多個線程, 串行的處理臨界區資源(一個代碼塊)
    互斥鎖相關函數:

      #include <pthread.h>
     
      // 初始化互斥鎖
      int pthread_mutex_init(pthread_mutex_t *restrict mutex,
                 const pthread_mutexattr_t *restrict attr);
          參數:
              - mutex: 互斥鎖的地址
              - attr: 互相鎖的屬性, 使用默認屬性, 賦值為NULL就可以
     
      // 釋放互斥鎖資源
      int pthread_mutex_destroy(pthread_mutex_t *mutex);
     
      // 將參數指定的互斥鎖上鎖
      // 比如: 3個線程, 第一個線程搶到了鎖, 對互斥鎖加鎖 -> 加鎖成功, 進入了臨界區
      //  第二,三個個線程也對這把鎖加鎖, 因為已經被線程1鎖定了, 線程2,3阻塞在了這把鎖上 -> 不能進入臨界區,
      //     當這把鎖被打開, 線程2,3解除阻塞, 線程2,3開始搶鎖, 誰搶到誰加鎖進入臨界區, 另一個繼續阻塞在鎖上
      int pthread_mutex_lock(pthread_mutex_t *mutex);
     
      // 嘗試加鎖, 如果這把鎖已經被鎖定了, 加鎖失敗, 函數直接返回, 不會阻塞在鎖上
      int pthread_mutex_trylock(pthread_mutex_t *mutex);
     
      // 解鎖函數
      int pthread_mutex_unlock(pthread_mutex_t *mutex);

其中:

      restrict: 修飾符, 被修飾過的變量特點: 不能被其他指針引用
          - mutex變量對應一塊內存
          - 舉例: pthread_mutex_t* ptr; ptr = &mutex; // error
          -  即便做了賦值, 使用ptr指針操作mutex對應的內存也是不允許的

 
3. 死鎖

兩個或兩個以上的進程在執行過程中,因爭奪共享資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。此時稱系統處於死鎖狀態或系統產生了死鎖 。

死鎖幾種場景:

    忘記釋放鎖,自己將自己鎖住
    單線程重復申請鎖
    多線程多鎖申請, 搶占鎖資源(線程A有一個鎖1,線程B有一個鎖2。線程A試圖調用lock來獲取鎖2就得掛起等待線程B釋放,線程B也調用lock試圖獲得鎖1。都在等對方釋放,然后獲得對方的鎖。)

 
4. 讀寫鎖

    讀寫鎖類型? 是幾把鎖?

          1. 讀寫鎖是一把鎖
          2. 鎖定讀操作, 鎖定寫操作
          3. 類型: pthread_rwlock_t

    讀寫鎖的特點

    /*
          1. 讀操作可以並進行, 多個線程
          2. 寫的時候獨占資源的
          3. 寫的優先級高於讀的優先級
    */
    場景:

              // 1. 線程A加讀鎖成功, 又來了三個線程, 做讀操作, 可以加鎖成功----讀操作是共享的, 三個新來的線程可以加讀鎖成功
              // 2. 線程A加寫鎖成功, 又來了三個線程, 做讀操作, 三個線程阻塞-------加讀鎖失敗, 會阻塞在讀鎖上, 寫完了
              // 3. 線程A加讀鎖成功, 又來了B線程加寫鎖阻塞, 又來了C線程加讀鎖阻塞------寫的獨占的, 寫的優先級高

    什么時候使用讀寫鎖?

    互斥鎖: 數據所有的讀寫都是串行的
    讀寫鎖:
           - 讀: 並行
           - 寫: 串行
      讀的頻率 > 寫的頻率

    操作函數:

      #include <pthread.h>
      // 初始化讀寫鎖
      int pthread_rwlock_init(pthread_rwlock_t *restrict rwlock,
                 const pthread_rwlockattr_t *restrict attr);
          參數:
              - rwlock: 讀寫鎖地址
              - attr: 讀寫鎖屬性, 使用默認屬性, 設置為: NULL
     
      // 釋放讀寫鎖資源
      int pthread_rwlock_destroy(pthread_rwlock_t *rwlock);
     
      // 加讀鎖
      // rwlock被加了寫鎖, 這時候阻塞
      // rwlock被加了讀鎖, 不阻塞, 可以加鎖成功 -> 讀共享
      int pthread_rwlock_rdlock(pthread_rwlock_t *rwlock);
     
      // 嘗試加讀鎖
      int pthread_rwlock_tryrdlock(pthread_rwlock_t *rwlock);
     
      // 加寫鎖
      // rwlock -> 加了讀鎖, 加了寫鎖 多會阻塞 -> 寫獨占
      int pthread_rwlock_wrlock(pthread_rwlock_t *rwlock);
     
      // 嘗試加寫鎖
      int pthread_rwlock_trywrlock(pthread_rwlock_t *rwlock);
     
      // 讀寫鎖解鎖
      int pthread_rwlock_unlock(pthread_rwlock_t *rwlock);

 

    練習例子:  8個線程操作同一個全局變量,其中3個線程不定時寫同一全局資源,其中5個線程不定時讀同一全局資源

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <errno.h>
    #include <pthread.h>
     
    int number = 1;
    pthread_rwlock_t rwlock;
     
    void* writeNum(void* arg)
    {
        while(1)
        {
            pthread_rwlock_wrlock(&rwlock);
            number++;
            usleep(100);
            printf("+++ write, tid: %ld, number: %d\n", pthread_self(), number);
            pthread_rwlock_unlock(&rwlock);
            usleep(100);
        }
        return NULL;
    }
     
    void* readNum(void* arg)
    {
        while(1)
        {
            pthread_rwlock_rdlock(&rwlock);
            printf("=== read, tid: %ld, number: %d\n", pthread_self(), number);
            pthread_rwlock_unlock(&rwlock);
            usleep(100);
        }
        return NULL;
    }
     
    int main(int argc, char *argv[])
    {
        pthread_t wtid[3], rtid[5];
        //初始化鎖
        pthread_rwlock_init(&rwlock, NULL);
        //創建寫進程
        for (int i=0; i<3; ++i)
        {
            pthread_create(&wtid[i],NULL, writeNum, NULL);
        }
        //創建讀進程
        for (int i=0; i<5; ++i)
        {
            pthread_create(&rtid[i], NULL, readNum, NULL);
        }
     
        //回收進程
        for (int i=0; i<3; ++i)
        {
            pthread_join(wtid[i], NULL);
        }
        for (int i=0; i<5; ++i)
        {
            pthread_join(rtid[i], NULL);
        }
        //銷毀鎖
        pthread_rwlock_destroy(&rwlock);
        return 0;
    }

 
5. 條件變量

    條件變量不是鎖
    條件變量兩個動作:

    條件變量能引起某個線程的阻塞具體來說就是:

           - 某個條件滿足之后, 阻塞線程
           - 某個條件滿足, 線程解除阻塞

    如果使用了條件變量進行線程同步, 多個線程操作了共享數據, 不能解決數據混亂問題,解決該問題, 需要配合使用互斥鎖

    條件變量類型

pthread_cond_t

    條件變量操作函數

    #include <pthread.h>
      // 初始化條件變量
      int pthread_cond_init(pthread_cond_t *restrict cond,const pthread_condattr_t *restrict attr);
          參數:
              - cond: 條件變量的地址
              - attr: 使用默認屬性, 這個值設置為NULL
     
      // 釋放資源
      int pthread_cond_destroy(pthread_cond_t *cond);
     
      // 線程調用該函數之后, 阻塞
      int pthread_cond_wait(pthread_cond_t *restrict cond,pthread_mutex_t *restrict mutex);
          參數:
              - cond: 條件變量
              - mutex: 互斥鎖
              
      struct timespec {
          time_t tv_sec;      /* Seconds */
          long   tv_nsec;     /* Nanoseconds [0 .. 999999999] */
       };
      // 在指定的時間之后解除阻塞
      int pthread_cond_timedwait(pthread_cond_t *restrict cond,
                 pthread_mutex_t *restrict mutex, const struct timespec *restrict abstime);
          參數:
              - cond: 條件變量
              - mutex: 互斥鎖
              - abstime: 阻塞的時間
                  - 當前時間 + 要阻塞的時長
                      struct timeval val;
                  可以使用函數:gettimeofday(&val, NULL);
     
      // 喚醒一個或多個阻塞在 pthread_cond_wait / pthread_cond_timedwait 函數上的線程
      int pthread_cond_signal(pthread_cond_t *cond);
     
      // 喚醒所有的阻塞在 pthread_cond_wait / pthread_cond_timedwait 函數上的線程
      int pthread_cond_broadcast(pthread_cond_t *cond);

 
5.1 生產者和消費者模型

角色分析:
      - 生產者
      - 消費者
      - 容器

    栗子:使用條件量實現 生產線和消費者模型: 生產者往鏈表中添加節點, 消費者刪除鏈表節點

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>
    #include <unistd.h>
    #include <errno.h>
    #include <pthread.h>
     
    pthread_cond_t cond;         //條件變量
    pthread_mutex_t mutex;       //互斥鎖
     
    //連表節點
    struct Node
    {
        int number;
        struct Node* next;
    };
     
    //指向鏈表第一個節點的指針
    struct Node* head = NULL;
     
    // 生產者函數、
    void* producer(void* arg)
    {
        while(1)
        {
            //創建新的鏈表節點
            pthread_mutex_lock(&mutex);
            struct Node* pnew = (struct Node*)malloc(sizeof(struct Node));
            pnew->next = head;
            head = pnew;
            pnew->number = rand() % 1000;
            printf("add+++ node, number: %d, tid = %ld\n", pnew->number, pthread_self());
            pthread_mutex_unlock(&mutex);
     
            //生產者生產了東西,通知消費者消費
            pthread_cond_signal(&cond);
        }
        return NULL;
    }
     
    //消費者函數
    void* customer(void* arg)
    {
        while(1)
        {
            pthread_mutex_lock(&mutex);
            while (head == NULL)
            {
                //鏈表為空,阻塞
                pthread_cond_wait(&cond, &mutex);
            }
     
            struct Node* pnode = head;
            head = head->next;
            printf("del--- node, number: %d, tid = %ld\n", pnode->number, pthread_self());
            free(pnode);
            pthread_mutex_unlock(&mutex);
        }
     
        return NULL;
    }
     
    int main(int argc, char *argv[])
    {
        pthread_t ptid[5], ctid[5];
        pthread_cond_init(&cond,NULL);
        pthread_mutex_init(&mutex,NULL);
     
        for (int i=0; i<5; ++i)
        {
            pthread_create(&ptid[i], NULL, producer, NULL);
            pthread_create(&ctid[i], NULL, customer, NULL);
        }
     
        for (int i=0; i<5; ++i)
        {
            pthread_join(ptid[i], NULL);
            pthread_join(ctid[i], NULL);
        }
        pthread_cond_destroy(&cond);
        pthread_mutex_destroy(&mutex);
        return 0;
    }

 
6. 信號量

    信號量用在多線程多任務同步的,一個線程完成了某一個動作就通過信號量告訴別的線程,別的線程再進行某些動作。
    信號量不一定是鎖定某一個資源,而是流程上的概念,比如:有A,B兩個線程,B線程要等A線程完成某一任務以后再進行自己下面的步驟,這個任務 並不一定是鎖定某一資源,還可以是進行一些計算或者數據處理之類。
    信號量(信號燈)與互斥鎖和條件變量的主要不同在於”燈”的概念,燈亮則意味着資源可用,燈滅則意味着不可用
    信號量主要阻塞線程, 不能完全保證線程安全.
     如果要保證線程安全, 需要信號量和互斥鎖一起使用.

 

- 信號量類型:

    sem_t
      在這個變量中記錄了一個整形數, 如果這個數據 是5, 允許有五個線程訪問數據
              o o o o o
              如果有一線程訪問了共享資源, 這個整形數 -1, 后邊又有4個線程訪問了共享數據 0,
              這時候, 再有線程訪問共享數據, 這些線程阻塞

- 信號量操作函數:

    #include <semaphore.h>
      // 初始化信號量
      int sem_init(sem_t *sem, int pshared, unsigned int value);
          參數:
              - sem: 信號量的地址
              - pshared: 0-> 處理線程, 1-> 處理進程
              - value: sem_t中整形數初始化
     
      // 釋放資源
      int sem_destroy(sem_t *sem);
     
      // 有可能引起阻塞
      // 調用一次這個函數 sem 中整形數 --
      // 當 sem_wait 並且 sem中的整形數為0 , 阻塞了
      int sem_wait(sem_t *sem);
     
      // 當 sem_trywait 並且 sem中的整形數為0 , 返回, 不阻塞
      int sem_trywait(sem_t *sem);
     
      // 當 sem_timedwait 並且 sem中的整形數為0 , 阻塞一定的時長, 時間到達, 返回
      int sem_timedwait(sem_t *sem, const struct timespec *abs_timeout);
     
      // 當 sem_post sem 中的整形數 ++
      int sem_post(sem_t *sem);
     
      // 查看 sem中的整形數的值, 通過第二個參數返回
      int sem_getvalue(sem_t *sem, int *sval);
————————————————
版權聲明:本文為CSDN博主「陳宸-研究僧」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_35883464/article/details/103547949


免責聲明!

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



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