在上一篇部落格互斥量中,解決了執行緒如何互斥訪問臨界資源的問題。
在開始本文之前,我們先保留一個問題:為什麼需要條件變數,如果只有互斥量不能解決什麼問題?
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 bycond
, and releases the mutex specified bymutex
. The waiting thread unblocks only after another thread callspthread_cond_signal
, orpthread_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 variablecond
(if any threads are blocked oncond
).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()
orpthread_cond_signal()
returns from its call topthread_cond_wait()
orpthread_cond_timedwait()
, the thread shall own themutex
with which it calledpthread_cond_wait()
orpthread_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 calledpthread_mutex_lock()
.The
pthread_cond_broadcast()
andpthread_cond_signal()
functions shall have no effect if there are no threads currently blocked oncond
.——Manual on Ubuntu.
喚醒一個在 cond
上等待的至少一個執行緒,如果 cond
上阻塞了多個執行緒,那麼將根據排程策略選取一個。
當被喚醒的執行緒從 wait/timedwait
函式返回,將重新獲得 mutex
(但可能需要競爭,因為可能喚醒多個執行緒)。
pthread_cond_broadcast
函式原型:
int pthread_cond_broadcast(pthread_cond_t *cond);
作用:喚醒所有在 cond
上等待的執行緒。
生產者消費者問題
又稱 PC (Producer - Consumer) 問題。
詳細問題定義可以看:
- [1] 百度百科:生產者消費者問題 .
- [2] 維基百科:Producer-Consumer Problem .
具體要求:
- 系統中有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
這是 produer
和 consumer
的結合體。
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_msg
中unlock
之後才喚醒等待的執行緒,會出現上述註釋出現的情況,造成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
的本意)。