多執行緒程式設計介紹-條件變數

ulysses發表於2019-05-13

多執行緒程式設計介紹-條件變數

條件變數定義

條件變數是多執行緒對共享資源資料的變化的通知機制。條件變數與互斥量明顯不同為互斥量是對臨界資源的保護機制,但條件變數可以理解為一種通訊機制。

條件變數的應用場景

設想如下程式設計場景,我們要實現一個訊息接收轉發並處理的流程,為了提高程式執行效率。我們啟動兩個執行緒一個是接收訊息執行緒,專門負責接收訊息,將訊息加入到一個共享連結串列中;而一個執行緒是工作執行緒,專門負責等待讀取連結串列中的訊息,如果連結串列為空,則工作執行緒則進入等待佇列,如果有節點插入則工作執行緒需要被喚醒繼續工作。

  • 問題1:共享資源訊息連結串列需要保護,怎麼做?

    互斥量是解決共享資源的典型方法,相信大家都知道,使用mutex鎖保護一下對連結串列的操作就好了。對於連結串列操作的程式碼段我們成為臨界資源。

  • 問題2:如何通知工作執行緒有訊息到達?

    方法1:大家應該都能想到,利用現有知識互斥量,工作執行緒一直輪詢去檢查連結串列中是否有訊息可以處理,不就可以了,但這樣顯然效率太低,浪費cpu資源。

    方法2:當有資源時接收訊息執行緒通知工作執行緒一下不就可以了,其他時間工作執行緒就休眠就好啦。是的,條件變數就是為了達到這個目的的。當工作執行緒檢查沒有訊息處理時,就主動將自己掛起,等待接收訊息執行緒喚醒。

條件變數功能函式介紹

int pthread_cond_init(pthread_cond_t *restrict cond, const pthread_condattr_t *restrict attr);
  • pthread_cond_init初始化一個條件變數,需要在其他條件變數執行函式之前執行。

  • 只能被初始化一次,如果初始化兩次則結果不可預期。

  • restrict cond 是一個條件變數地址,使用此函式前需要定義一個條件變數結構體pthread_cond_t

  • 如果需要copy條件變數,只能copy條件變數地址,不能複製這個結構體,否則結果將不可預知。(想想也知道這個條件變數是一個結構體,如果複製一個結構體則資訊被複制兩份在多執行緒中一定會出現問題,而複製地址則原內容不變)

  • 第二個欄位屬性欄位,一般預設填NULL(其他高階用法待研究,如果有知道的小夥伴還望分享一下啊~)

    int pthread_cond_destroy(pthread_cond_t *cond);
  • 釋放一個條件變數,一般值只線上程結束時呼叫,用於回收條件變數資源。

    int pthread_cond_timedwait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex,
    const struct timespec *restrict abstime);
    int pthread_cond_wait(pthread_cond_t *restrict cond, pthread_mutex_t *restrict mutex);
  • 這兩個函式用於主動將執行緒阻塞,等待其他執行緒喚醒。對應場景中工作執行緒判斷沒有可以獲取的訊息時將執行緒主動放入阻塞佇列中(這裡的阻塞佇列指作業系統中的),即執行緒將自己掛起。

  • 函式中前兩個引數一個是條件變數本身,一個是mutex互斥量。條件變數沒什麼好說的,因為這組函式就是和他相關的,那mutex呢?mutex是用來保護臨界資源使用的mutex,如場景中所說的對於訊息佇列的訪問是需要加鎖保護的。之所以要出入mutex是因為執行緒在阻塞掛起自己之前要先釋放鎖,不然其他執行緒也不能獲取鎖了。

  • 根據上一點分析mutex不但要作為入參傳入,而且需要在傳入之前先獲得鎖。

  • 當然pthread_cond_timedwait函式的最後一個引數可以指定阻塞的時間,即即使在指定的時間沒沒有執行緒喚醒這個阻塞執行緒,阻塞執行緒也會自己被喚醒執行。

    int pthread_cond_broadcast(pthread_cond_t *cond);
    int pthread_cond_signal(pthread_cond_t *cond);
  • 以上這兩個函式是用來向等待此條件變數的執行緒傳送訊號,表示可以繼續執行了。對應我們的場景就是接收執行緒有訊息到達,之後將訊息插入佇列,通知工作執行緒。

  • 兩個函式不同之處,pthread_cond_broadcast喚醒等待中的所有執行緒。pthread_cond_signal至少喚醒一個等待執行緒。

  • 如果執行這兩個函式的時候沒有任何等待此條件變數的執行緒,則無任何影響,也無任何變化。這一點在後續問題介紹時會再次提到。

條件變數程式設計例項

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

#define MSG_LEN_MAX        31

typedef struct tagNode
{
    char szMsg[MSG_LEN_MAX+1];
    struct tagNode *pstNextNode;
}NODE_S;

typedef struct tagList
{
    pthread_mutex_t mutex;
    pthread_cond_t cond;
    NODE_S stHead;
}LIST_S;

LIST_S g_stMsgList = {};

void list_Init(LIST_S *pstList)
{
    pstList->stHead.pstNextNode = NULL;
    pthread_mutex_init(&pstList->mutex, NULL);
    pthread_cond_init(&pstList->cond, NULL);
}
/*工作執行緒用於處理並列印訊息*/
void * dealMsgThread(void * pVoid)
{
    NODE_S *pstNode = NULL;
    int count = 0;
    
    while (1)
    {
        pthread_mutex_lock(&g_stMsgList.mutex);        
        
        /* 查詢連結串列中的節點的頭節點返回 */
        if ( NULL == g_stMsgList.stHead.pstNextNode )
        {
            printf("can not find msg,waiting...
");
            pthread_cond_wait(&g_stMsgList.cond, &g_stMsgList.mutex);
            printf("notify msg, wake up
");
        }

        if ( NULL != g_stMsgList.stHead.pstNextNode )
        {
            pstNode = g_stMsgList.stHead.pstNextNode;
            g_stMsgList.stHead.pstNextNode = pstNode->pstNextNode;
            printf("echo: %s
", pstNode->szMsg);
            free(pstNode);
            count++;
        }

        pthread_mutex_unlock(&g_stMsgList.mutex);
        if ( count >= 10 )
        {
            break;
        }
    }

    return NULL;
}
/*向訊息佇列中放入節點,並通知等待的工執行緒*/
void receiveMsg(char *pcMsg, int iLen)
{
    NODE_S *pstNode = NULL;
    NODE_S* pstTemp = NULL;
    
    if ( iLen > MSG_LEN_MAX )
    {
        iLen = MSG_LEN_MAX;
    }

    pstNode = (NODE_S*)malloc(sizeof(NODE_S));
    memset(pstNode, 0, sizeof(NODE_S));
    snprintf(pstNode->szMsg, MSG_LEN_MAX+1, "%s", pcMsg);

    /* 獲取鎖 */
    pthread_mutex_lock(&g_stMsgList.mutex);

    pstTemp = &g_stMsgList.stHead;
    while ( pstTemp )
    {
        if ( NULL == pstTemp->pstNextNode )
        {
            break;
        }

        pstTemp = pstTemp->pstNextNode;
    }

    pstTemp->pstNextNode = pstNode;
    
    printf("recieve msg %s add list, send signal
", pcMsg);
    /* 傳送通知到工作執行緒 */
    pthread_cond_signal(&g_stMsgList.cond);

    pthread_mutex_unlock(&g_stMsgList.mutex);

    return;
}
/*main函式*/
int main(int argc, char **argv)
{
    pthread_t iThreadId;
    void *ret = NULL;
    char szMsg[MSG_LEN_MAX+1];
    
    list_Init(&g_stMsgList);
    
    pthread_create(&iThreadId, NULL, dealMsgThread, NULL);
    //sleep(1);
    for(int i =0 ; i < 10; i++)
    {
        sprintf(szMsg, "%d : hello", i);
        receiveMsg(szMsg, strlen(szMsg));
    }
    
    pthread_join(iThreadId, &ret);

    return 0;
}
  • 輸出結果:

    • recieve msg 0 : hello add list, send signal
      recieve msg 1 : hello add list, send signal
      recieve msg 2 : hello add list, send signal
      recieve msg 3 : hello add list, send signal
      recieve msg 4 : hello add list, send signal
      recieve msg 5 : hello add list, send signal
      recieve msg 6 : hello add list, send signal
      recieve msg 7 : hello add list, send signal
      echo: 0 : hello
      echo: 1 : hello
      echo: 2 : hello
      echo: 3 : hello
      echo: 4 : hello
      echo: 5 : hello
      echo: 6 : hello
      echo: 7 : hello
      can not find msg,waiting…
      recieve msg 8 : hello add list, send signal
      recieve msg 9 : hello add list, send signal
      notify msg, wake up
      echo: 8 : hello
      echo: 9 : hello

條件變數函式內部實現猜想

1.條件變數內部猜想一:條件變數pthread_cond_timedwait函式內部對於條件變數本身還存在一個鎖。

  • 這個鎖的用途就是保證條件變數掛起和釋放鎖是一個原子操作。

  • 設想場景,如果進入pthread_cond_timedwait函式之後,先釋放mutex(必須執行釋放操作,前文提到),之後再進入等待掛起之前,cpu切換到執行pthread_cond_signal執行緒,此時由於沒有等待執行緒,從而不會有任何影響,這就導致後續進入wait態的工作執行緒永遠等不到被接收執行緒喚醒。所以為保證釋放和進入wait態不能被打斷,所以需要加鎖保護。

  • 當然不只是猜想,通過一篇核心態對於pthread_cond_timedwait函式的分析也證實了這一點。連結:https://www.cnblogs.com/c-slm…

條件變數使用注意事項

  • 條件變數使用前初始化,且初始化一次。

  • 條件變數使用pthread_cond_timedwait類函式時需要在獲取mutex鎖和釋放mutex鎖之間。

  • 使用pthread_cond_timedwait函式被喚醒後仍然需要判斷佇列中的狀態,因為可能被其他執行緒首先搶佔了mutex並處理了訊息佇列訊息。即等待的條件變數失效,需要重新等待。

  • 條件變數使用pthread_cond_signal類函式時可以在獲取mutex鎖和釋放mutex鎖之間,也可以在獲取mutex鎖和釋放mutex鎖之後。但建議在獲取mutex鎖和釋放mutex鎖之間。

    • pthread_cond_signal函式在mutex lock與unlock之間執行。

      • 缺點:可能導致pthread_cond_wait執行緒執行後重新進入休眠,因為wait執行緒需要獲取mutex鎖,但此時signal執行緒可能並沒有釋放,導致頻繁的cpu切換。

    • pthread_cond_signal函式在mutex lock,unlock之後執行。

      • 缺點:先unlock操作之後此時低優先順序任務可能會佔用cpu資源導致wait的高優先順序任務得不到排程。因為wait的函式還沒有收到signal訊號喚醒。

相關文章