pthread 條件變數

sinkinben發表於2020-12-04

在上一篇部落格互斥量中,解決了執行緒如何互斥訪問臨界資源的問題。

在開始本文之前,我們先保留一個問題:為什麼需要條件變數,如果只有互斥量不能解決什麼問題?

API

init/destroy

條件變數的資料型別是 pthread_cond_t .

初始化,銷燬 API 為:

int pthread_cond_init(pthread_cond_t *cond, const pthread_condattr_t *attr);
int pthread_cond_destroy(pthread_cond_t *cond);

pthread_cond_wait

函式原型:

int pthread_cond_wait(pthread_cond_t *cond, pthread_mutex_t *mutex);

作用:

The pthread_cond_wait function atomically blocks the current thread waiting on the condition variable specified by cond, and releases the mutex specified by mutex. The waiting thread unblocks only after another thread calls pthread_cond_signal, or pthread_cond_broadcast with the same condition variable, and the current thread re-acquires the lock on mutex.

—— Manual on MacOS.

在條件變數 cond 上阻塞執行緒,加入 cond 的等待佇列,並釋放互斥量 mutex . 如果其他執行緒使用同一個條件變數 cond 呼叫了 pthread_cond_signal/broadcast ,喚醒的執行緒會重新獲得互斥鎖 mutex .

pthread_cond_timedwait

函式原型:

int pthread_cond_timedwait(pthread_cond_t *cond, pthread_mutex_t *mutex, const struct timespec *abstime);

作用:與 pthread_cond_wait 類似,但該執行緒被喚醒的條件是其他執行緒呼叫了 signal/broad ,或者系統時間到達了 abstime

pthread_cond_signal

函式原型:

int pthread_cond_signal(pthread_cond_t *cond)

作用:

The pthread_cond_signal function shall unblock at least one of the threads that are blocked no the specified condition variable cond (if any threads are blocked on cond).

If more than one thread is blocked on a condition variable, the scheduling policy shall determine the order which threads are unblocked.

When each thread unblocked as a result of a pthread_cond_broadcast() or pthread_cond_signal() returns from its call to pthread_cond_wait() or pthread_cond_timedwait(), the thread shall own the mutex with which it called pthread_cond_wait() or pthread_cond_timedwait().

The thread(s) that are unblocked shall contend for the mutex according to the scheduling policy (if applicable), and as if each had called pthread_mutex_lock().

The pthread_cond_broadcast() and pthread_cond_signal() functions shall have no effect if there are no threads currently blocked on cond.

——Manual on Ubuntu.

喚醒一個在 cond 上等待的至少一個執行緒,如果 cond 上阻塞了多個執行緒,那麼將根據排程策略選取一個。

當被喚醒的執行緒從 wait/timedwait 函式返回,將重新獲得 mutex (但可能需要競爭,因為可能喚醒多個執行緒)。

pthread_cond_broadcast

函式原型:

int pthread_cond_broadcast(pthread_cond_t *cond);

作用:喚醒所有在 cond 上等待的執行緒。

生產者消費者問題

又稱 PC (Producer - Consumer) 問題。

詳細問題定義可以看:

具體要求:

  • 系統中有3個執行緒:生產者、計算者、消費者
  • 系統中有2個容量為4的緩衝區:buffer1、buffer2
  • 生產者生產'a'、'b'、'c'、‘d'、'e'、'f'、'g'、'h'八個字元,放入到buffer1; 計算者從buffer1取出字元,將小寫字元轉換為大寫字元,放入到buffer2
  • 消費者從buffer2取出字元,將其列印到螢幕上

buffer 的定義

buffer 實質上是一個佇列。

const int CAPACITY = 4;
typedef struct
{
    char items[CAPACITY];
    int in, out;
} buffer_t;
void buffer_init(buffer_t *b) { b->in = b->out = 0; }
int buffer_is_full(buffer_t *b) { return ((b->in + 1) % CAPACITY) == (b->out); }
int buffer_is_empty(buffer_t *b) { return b->in == b->out; }
void buffer_put_item(buffer_t *buf, char item)
{
    buf->items[buf->in] = item;
    buf->in = (buf->in + 1) % CAPACITY;
}
char buffer_get_item(buffer_t *buf)
{
    char item = buf->items[buf->out];
    buf->out = (buf->out + 1) % CAPACITY;
    return item;
}

一些全域性變數

const int CAPACITY = 4;  // buffer 的容量
const int N = 8;         // 依據題意,需要轉換 8 個字元
buffer_t buf1, buf2;
pthread_mutex_t mutex1 = PTHREAD_MUTEX_INITIALIZER;  // 保證只有一個執行緒訪問 buf1
pthread_mutex_t mutex2 = PTHREAD_MUTEX_INITIALIZER;  // 保證只有一個執行緒訪問 buf2
pthread_cond_t empty1 = PTHREAD_COND_INITIALIZER;
pthread_cond_t empty2 = PTHREAD_COND_INITIALIZER;
pthread_cond_t full1 = PTHREAD_COND_INITIALIZER;
pthread_cond_t full2 = PTHREAD_COND_INITIALIZER;

幾個條件變數的作用如下:

  • empty1 表示當 buf1 為空的時候,從 buf1 取資料的執行緒要在此條件變數上等待。
  • full1 表示當 buf1 為滿的時候,向 buf1 寫資料的執行緒要在此條件變數上等待。

其他同理。

producer

程式碼思路解析:

  • 因為要對 buf1 操作,首先寫一對 pthread_mutex_lock/unlock ,保證臨界程式碼區內只有 producer 操作 buf1
  • 如果 buf1 是滿的,那麼就將 producer 執行緒阻塞在條件變數 full1 上,釋放互斥量 mutex1 (雖然不能寫入,但要讓別的執行緒能夠讀取 buf1 的資料);
  • 進入臨界區,把資料寫入 buf1
  • 離開臨界區,因為寫入了一次資料,buf1 必定不為空,因此喚醒一個在 empty1 上等待的執行緒,最後釋放 mutex1 .
void *producer(void *arg)
{
    int i = 0;
    // can be while(true)
    for (; i < N; i++)
    {
        pthread_mutex_lock(&mutex1);
        while (buffer_is_full(&buf1))
            pthread_cond_wait(&full1, &mutex1);
        buffer_put_item(&buf1, (char)('a' + i));
        printf("Producer put [%c] in buffer1. \n", (char)('a' + i));
        pthread_cond_signal(&empty1);
        pthread_mutex_unlock(&mutex1);
    }
    return NULL;
}

consumer

思路與 producer 類似。

void *consumer(void *arg)
{
    int i = 0;
    for (; i < N; i++)
    {
        pthread_mutex_lock(&mutex2);
        while (buffer_is_empty(&buf2))
            pthread_cond_wait(&empty2, &mutex2);
        char item = buffer_get_item(&buf2);
        printf("\tConsumer get [%c] from buffer2. \n", item);
        pthread_cond_signal(&full2);
        pthread_mutex_unlock(&mutex2);
    }
    return NULL;
}

calculator

這是 produerconsumer 的結合體。

void *calculator(void *arg)
{
    int i = 0;
    char item;
    for (; i < N; i++)
    {
        pthread_mutex_lock(&mutex1);
        while (buffer_is_empty(&buf1))
            pthread_cond_wait(&empty1, &mutex1);
        item = buffer_get_item(&buf1);
        pthread_cond_signal(&full1);
        pthread_mutex_unlock(&mutex1);

        pthread_mutex_lock(&mutex2);
        while (buffer_is_full(&buf2))
            pthread_cond_wait(&full2, &mutex2);
        buffer_put_item(&buf2, item - 'a' + 'A');
        pthread_cond_signal(&empty2);
        pthread_mutex_unlock(&mutex2);
    }
    return NULL;
}

main 函式

int main()
{
    pthread_t calc, prod, cons;
    // init buffer
    buffer_init(&buf1), buffer_init(&buf2);
	// create threads
    pthread_create(&calc, NULL, calculator, NULL);
    pthread_create(&prod, NULL, producer, NULL);
    pthread_create(&cons, NULL, consumer, NULL);
    pthread_join(calc, NULL);
    pthread_join(prod, NULL);
    pthread_join(cons, NULL);
    // destroy mutex
    pthread_mutex_destroy(&mutex1), pthread_mutex_destroy(&mutex2);
    // destroy cond
    pthread_cond_destroy(&empty1), pthread_cond_destroy(&empty2);
    pthread_cond_destroy(&full1), pthread_cond_destroy(&full2);
}

為什麼需要條件變數

從上面的 Producer - Consumer 問題可以看出,mutex 僅僅能表達「執行緒能否獲得訪問臨界資源的許可權」這一層面的資訊,而不能表達「臨界資源是否足夠」這個問題。

假設沒有條件變數,producer 執行緒獲得了 buf1 的訪問許可權( buf1 的空閒位置對於 producer 來說是一種資源),但如果 buf1 是滿的,producer 就沒法對 buf1 操作。

對於 producer 來說,它不能佔用訪問 buf1 的互斥鎖,但卻又什麼都不做。因此,它只能釋放互斥鎖 mutex ,讓別的執行緒能夠訪問 buf1 ,並取走資料,等到 buf1 有空閒位置,producer 再對 buf1 寫資料。用虛擬碼表述如下:

pthread_mutex_lock(&mutex1);
if (buffer_is_full(&buf1))
{
    pthread_mutex_unlock(&mutex1);
    wait_until_not_full(&buf1);
    pthread_mutex_lock(&mutex);
}
buffer_put_item(&buf1, item);
pthread_mutex_unlock(&mutex1);

而條件變數實際上是對上述一系列操作的一種封裝。

為什麼是 while

在上面程式碼中,使用 pthread_cond_wait 的時候,我們是通過這樣的方式的:

while (...)
    pthread_cond_wait(&cond, &mutex);

但這裡為什麼不是 if 而是 while 呢?

參考文章:https://www.cnblogs.com/leijiangtao/p/4028338.html

解釋 1

#include <pthread.h>
struct msg {
  struct msg *m_next;
  /* value...*/
};
 
struct msg* workq;
pthread_cond_t qready = PTHREAD_COND_INITIALIZER;
pthread_mutex_t qlock = PTHREAD_MUTEX_INITIALIZER;
 
void
process_msg() {
  struct msg* mp;
  for (;;) {
    pthread_mutex_lock(&qlock);
    while (workq == NULL) {
      pthread_cond_wait(&qread, &qlock);
    }
    mq = workq;
    workq = mp->m_next;
    pthread_mutex_unlock(&qlock);
    /* now process the message mp */
  }
}
 
void
enqueue_msg(struct msg* mp) {
    pthread_mutex_lock(&qlock);
    mp->m_next = workq;
    workq = mp;
    pthread_mutex_unlock(&qlock);
    /** 此時第三個執行緒在signal之前,執行了process_msg,剛好把mp元素拿走*/
    pthread_cond_signal(&qready);
    /** 此時執行signal, 在pthread_cond_wait等待的執行緒被喚醒,
        但是mp元素已經被另外一個執行緒拿走,所以,workq還是NULL ,因此需要繼續等待*/
}

程式碼解析:

這裡 process_msg 相當於消費者,enqueue_msg 相當於生產者,struct msg* workq 作為緩衝佇列。

process_msg 中使用 while (workq == NULL) 迴圈判斷條件,這裡主要是因為在 enqueue_msgunlock 之後才喚醒等待的執行緒,會出現上述註釋出現的情況,造成 workq==NULL,因此需要繼續等待。

但是如果將 pthread_cond_signal 移到 pthread_mutex_unlock() 之前執行,則會避免這種競爭,在 unlock 之後,會首先喚醒 pthread_cond_wait 的執行緒,進而 workq != NULL 總是成立。

因此建議使用 while 迴圈進行驗證,以便能夠容忍這種競爭。

解釋 2

pthread_cond_signal 在多核處理器上可能同時喚醒多個執行緒。

//thread 1
while(0<x<10)
    pthread_cond_wait(...);

//thread 2
while(5<x<15)
    pthread_cond_wait(...);

如果某段時間內 x == 8,那麼兩個執行緒相繼進入等待。

然後第三個執行緒,進行了如下操作:

x = 12
pthread_cond_signal(...) 
// or call pthread_cond_broadcast()

那麼可能執行緒 1、2 都被喚醒了(因為 signal 可能喚醒多個),但是,此時執行緒 1 仍然不滿足 while,需要再次判斷,然後進入下一次等待。

其次,即使 signal 只喚醒一個,上面我們提到,如果有多個執行緒都阻塞在同一個 cond 上,signal 會根據排程策略選取一個喚醒,那如果根據排程策略,喚醒的是執行緒 1 ,顯然它還需要再一次判斷是否需要繼續等待(否則就違背了 pthead_cond_wait 的本意)。

相關文章