# 同步,第 5 部分:條件變量
> 原文:<https://github.com/angrave/SystemProgramming/wiki/Synchronization%2C-Part-5%3A-Condition-Variables>
## 條件變量簡介
## 暖身
命名這些屬性!
* “CS 中一次只能有一個進程(/ thread)”
* “如果等待,那么另一個進程只能進入有限次數的 CS”
* “如果 CS 中沒有其他進程,那么進程可以立即進入 CS”
有關答案,請參見[同步,第 4 部分:臨界區問題](/angrave/SystemProgramming/wiki/Synchronization%2C-Part-4%3A-The-Critical-Section-Problem)。
## 什么是條件變量?你怎么用它們?什么是虛假喚醒?
* 條件變量允許一組線程睡眠直到發癢!你可以勾選一個線程或所有正在休眠的線程。如果您只喚醒一個線程,那么操作系統將決定喚醒哪個線程。你不直接喚醒線程,而是“發出”條件變量,然后喚醒條件變量內部的一個(或所有)線程。
* 條件變量與互斥鎖和循環一起使用(以檢查條件)。
* 偶爾等待的線程可能會無緣無故地喚醒(這被稱為 _ 虛假喚醒 _)!這不是問題,因為您總是在循環中使用`wait`來測試必須為 true 才能繼續的條件。
* 通過調用`pthread_cond_broadcast`(全部喚醒)或`pthread_cond_signal`(喚醒一個)喚醒在條件變量內睡眠的線程。注意盡管有函數名稱,這與 POSIX `signal`無關!
## `pthread_cond_wait`有什么作用?
調用`pthread_cond_wait`執行三個操作:
* 解鎖互斥鎖
* 等待(在相同的條件變量上調用`pthread_cond_signal`時休眠)
* 在返回之前,鎖定互斥鎖
## (高級主題)為什么條件變量也需要互斥鎖?
條件變量需要互斥鎖有三個原因。最簡單的理解是它可以防止早期喚醒消息(`signal`或`broadcast`功能)被“丟失”。想象一下,在調用 _ `pthread_cond_wait`之前,滿足條件的下列事件序列(時間向下運行)。在這個例子中,喚醒信號丟失了!
| 線程 1 | 線程 2 |
| --- | --- |
| `while( answer < 42) {` | |
| | `answer++` |
| | `p_cond_signal(cv)` |
| `p_cond_wait(cv,m)` | |
如果兩個線程都鎖定了互斥鎖,則在 `pthread_cond_wait(cv, m)`被調用(然后在內部解鎖互斥鎖之后)_ 之前無法發送信號 _
第二個常見原因是更新程序狀態(`answer`變量)通常需要互斥 - 例如,多個線程可能正在更新`answer`的值。
第三個也是微妙的原因是為了滿足我們在此僅概述的實時調度問題:在時間關鍵型應用中,應該允許具有 _ 最高優先級 _ 的等待線程首先繼續。為滿足此要求,還必須在調用`pthread_cond_signal`或`pthread_cond_broadcast`之前鎖定互斥鎖。對于好奇的人來說,[在](https://groups.google.com/forum/?hl=ky#!msg/comp.programming.threads/wEUgPq541v8/ZByyyS8acqMJ)[中進行了較長時間的歷史性討論。](https://groups.google.com/forum/?hl=ky#!msg/comp.programming.threads/wEUgPq541v8/ZByyyS8acqMJ)
## 為什么存在虛假的尾流?
為了表現。在多 CP??U 系統上,競爭條件可能導致喚醒(信號)請求被忽視。內核可能無法檢測到此丟失的喚醒呼叫,但可以檢測到它何時可能發生。為了避免潛在的丟失信號,線程被喚醒,以便程序代碼可以再次測試條件。
## 例
條件變量 _ 總是 _ 與互斥鎖一起使用。
在調用 _ 等待 _ 之前,必須鎖定互斥鎖并且 _ 等待 _ 必須用循環包裹。
```c
pthread_cond_t cv;
pthread_mutex_t m;
int count;
// Initialize
pthread_cond_init(&cv, NULL);
pthread_mutex_init(&m, NULL);
count = 0;
pthread_mutex_lock(&m);
while (count < 10) {
pthread_cond_wait(&cv, &m);
/* Remember that cond_wait unlocks the mutex before blocking (waiting)! */
/* After unlocking, other threads can claim the mutex. */
/* When this thread is later woken it will */
/* re-lock the mutex before returning */
}
pthread_mutex_unlock(&m);
//later clean up with pthread_cond_destroy(&cv); and mutex_destroy
// In another thread increment count:
while (1) {
pthread_mutex_lock(&m);
count++;
pthread_cond_signal(&cv);
/* Even though the other thread is woken up it cannot not return */
/* from pthread_cond_wait until we have unlocked the mutex. This is */
/* a good thing! In fact, it is usually the best practice to call */
/* cond_signal or cond_broadcast before unlocking the mutex */
pthread_mutex_unlock(&m);
}
```
## 實現計數信號量
* 我們可以使用條件變量實現計數信號量。
* 每個信號量都需要一個計數,一個條件變量和一個互斥量
```c
typedef struct sem_t {
int count;
pthread_mutex_t m;
pthread_condition_t cv;
} sem_t;
```
實現`sem_init`以初始化互斥鎖和條件變量
```c
int sem_init(sem_t *s, int pshared, int value) {
if (pshared) { errno = ENOSYS /* 'Not implemented'*/; return -1;}
s->count = value;
pthread_mutex_init(&s->m, NULL);
pthread_cond_init(&s->cv, NULL);
return 0;
}
```
我們`sem_post`的實現需要增加計數。我們還將喚醒在條件變量內部休眠的任何線程。請注意,我們鎖定和解鎖互斥鎖,因此一次只有一個線程可以在臨界區內。
```c
sem_post(sem_t *s) {
pthread_mutex_lock(&s->m);
s->count++;
pthread_cond_signal(&s->cv); /* See note */
/* A woken thread must acquire the lock, so it will also have to wait until we call unlock*/
pthread_mutex_unlock(&s->m);
}
```
如果信號量的計數為零,我們的`sem_wait`實現可能需要休眠。就像`sem_post`一樣,我們使用鎖來包裝臨界區(因此一次只有一個線程可以執行我們的代碼)。請注意,如果線程確實需要等待,那么互斥鎖將被解鎖,允許另一個線程進入`sem_post`并從我們的睡眠中喚醒我們!
請注意,即使線程被喚醒,在它從`pthread_cond_wait`返回之前,它必須重新獲取鎖,因此它必須再等一點(例如,直到 sem_post 結束)。
```c
sem_wait(sem_t *s) {
pthread_mutex_lock(&s->m);
while (s->count == 0) {
pthread_cond_wait(&s->cv, &s->m); /*unlock mutex, wait, relock mutex*/
}
s->count--;
pthread_mutex_unlock(&s->m);
}
```
**等`sem_post`一直調用`pthread_cond_signal`不會破壞 sem_wait?** 答案:不!在計數非零之前,我們無法通過循環。在實踐中,這意味著即使沒有等待線程,`sem_post`也會不必要地調用`pthread_cond_signal`。更有效的實施只會在必要時調用`pthread_cond_signal`,即
```c
/* Did we increment from zero to one- time to signal a thread sleeping inside sem_post */
if (s->count == 1) /* Wake up one waiting thread!*/
pthread_cond_signal(&s->cv);
```
## 其他信號量考慮因素
* 真實的信號量實現包括隊列和調度問題,以確保公平性和優先級,例如喚醒最高優先級的最長睡眠線程。
* 此外,`sem_init`的高級使用允許跨進程共享信號量。我們的實現僅適用于同一進程內的線程。
- UIUC CS241 系統編程中文講義
- 0. 簡介
- #Informal 詞匯表
- #Piazza:何時以及如何尋求幫助
- 編程技巧,第 1 部分
- 系統編程短篇小說和歌曲
- 1.學習 C
- C 編程,第 1 部分:簡介
- C 編程,第 2 部分:文本輸入和輸出
- C 編程,第 3 部分:常見問題
- C 編程,第 4 部分:字符串和結構
- C 編程,第 5 部分:調試
- C 編程,復習題
- 2.進程
- 進程,第 1 部分:簡介
- 分叉,第 1 部分:簡介
- 分叉,第 2 部分:Fork,Exec,等等
- 進程控制,第 1 部分:使用信號等待宏
- 進程復習題
- 3.內存和分配器
- 內存,第 1 部分:堆內存簡介
- 內存,第 2 部分:實現內存分配器
- 內存,第 3 部分:粉碎堆棧示例
- 內存復習題
- 4.介紹 Pthreads
- Pthreads,第 1 部分:簡介
- Pthreads,第 2 部分:實踐中的用法
- Pthreads,第 3 部分:并行問題(獎金)
- Pthread 復習題
- 5.同步
- 同步,第 1 部分:互斥鎖
- 同步,第 2 部分:計算信號量
- 同步,第 3 部分:使用互斥鎖和信號量
- 同步,第 4 部分:臨界區問題
- 同步,第 5 部分:條件變量
- 同步,第 6 部分:實現障礙
- 同步,第 7 部分:讀者編寫器問題
- 同步,第 8 部分:環形緩沖區示例
- 同步復習題
- 6.死鎖
- 死鎖,第 1 部分:資源分配圖
- 死鎖,第 2 部分:死鎖條件
- 死鎖,第 3 部分:餐飲哲學家
- 死鎖復習題
- 7.進程間通信&amp;調度
- 虛擬內存,第 1 部分:虛擬內存簡介
- 管道,第 1 部分:管道介紹
- 管道,第 2 部分:管道編程秘密
- 文件,第 1 部分:使用文件
- 調度,第 1 部分:調度過程
- 調度,第 2 部分:調度過程:算法
- IPC 復習題
- 8.網絡
- POSIX,第 1 部分:錯誤處理
- 網絡,第 1 部分:簡介
- 網絡,第 2 部分:使用 getaddrinfo
- 網絡,第 3 部分:構建一個簡單的 TCP 客戶端
- 網絡,第 4 部分:構建一個簡單的 TCP 服務器
- 網絡,第 5 部分:關閉端口,重用端口和其他技巧
- 網絡,第 6 部分:創建 UDP 服務器
- 網絡,第 7 部分:非阻塞 I O,select()和 epoll
- RPC,第 1 部分:遠程過程調用簡介
- 網絡復習題
- 9.文件系統
- 文件系統,第 1 部分:簡介
- 文件系統,第 2 部分:文件是 inode(其他一切只是數據...)
- 文件系統,第 3 部分:權限
- 文件系統,第 4 部分:使用目錄
- 文件系統,第 5 部分:虛擬文件系統
- 文件系統,第 6 部分:內存映射文件和共享內存
- 文件系統,第 7 部分:可擴展且可靠的文件系統
- 文件系統,第 8 部分:從 Android 設備中刪除預裝的惡意軟件
- 文件系統,第 9 部分:磁盤塊示例
- 文件系統復習題
- 10.信號
- 過程控制,第 1 部分:使用信號等待宏
- 信號,第 2 部分:待處理的信號和信號掩碼
- 信號,第 3 部分:提高信號
- 信號,第 4 部分:信號
- 信號復習題
- 考試練習題
- 考試主題
- C 編程:復習題
- 多線程編程:復習題
- 同步概念:復習題
- 記憶:復習題
- 管道:復習題
- 文件系統:復習題
- 網絡:復習題
- 信號:復習題
- 系統編程笑話