# 並行程式的潛在問題(三) :::info - 🚀 更多文章都已經整合到 [AwesomeCS Wiki](https://github.com/ianchen0119/AwesomeCS/wiki) 了,歡迎送上 Star ! - 🚀 [AwesomeCS 臉書專業](https://www.facebook.com/AwesomeComputerScience/) ::: 在介紹 Mutex lock 與 spinlock 後,本篇文章同樣針對並行程式的 Synchronization 作探討,學習如何保證並行程式的執行順序並介紹什麼是 Deadlock 與 Livelock 。 ## Semaphore **Semaphore** 中文稱為號誌,是一個同步物件,用於保持在 0 至指定最大值之間的一個計數值。當執行緒完成一次對該 semaphore 物件的等待(wait)時,該計數值減一;當執行緒完成一次對 semaphore 物件的釋放(release)時,計數值加一。當計數值為 0 ,則執行緒等待該 semaphore 物件不再能成功直至該 semaphore 物件變成 signaled 狀態。 semaphore 物件的計數值大於 0 ,為 signaled 狀態;計數值等於 0 ,為 nonsignaled 狀態. -- 維基百科 ### Semaphore APIs 使用 Semaphore 前須詳閱公開說明書: ```c= sem_t semaphore; int sem_init(sem_t *sem, int pshared, unsigned int value); int sem_wait(sem_t *sem); int sem_post(sem_t *sem); ``` 在使用 semaphore 前,我們需要使用 `sem_init()` 將其初始化: ```c= static sem_t semaphore; sem_init(&semaphore, 0, 1); ``` 透過 [Linux manual page](https://man7.org/linux/man-pages/man3/sem_init.3.html) 查看 `sem_init()` 的定義,可以知道它的三個參數從左至右分別為: - `sem_t` 型別變數的位址。 - 決定 semaphore 共享於同一個 Process 的 Thread ,或是多個 Process 。 - semaphore 的初始值。 初始化後,通常我們會在 Critical section 間放置 `sem_wait()` 與 `sem_post()` 去避免 Race condition 。 #### sem_wait() `sem_wait()` 會檢查傳入的 semaphore 變數值,如果大於零,則會將它減一。相反的,如果變數值為零,該執行緒就會被 Block 。 #### sem_post() 當執行緒處理完 Critical section 便可以使用 `sem_post()` 釋放 Semaphore 物件。這時,傳入的 Semaphore 物件值會被加一。 ### 透過範例程式學習 Semaphore! 比起 Mutex ,雖然 Semaphore 同樣可以用來保護 Critical section ,不過它更常被用來確保並行程式的執行順序。不像是 Mutex lock **解鈴還需繫鈴人**的特性,在 Semaphore 中,每個任務都會消耗與製造不同的號誌,考慮以下程式碼: ```c= static sem_t A_B; static sem_t B_C; static sem_t C_A; void *printA(void *arg) { int i = 0; for(i = 1;i<11;i++){ sem_wait(&C_A); printf("第%02d次:A",i); sem_post(&A_B); } return NULL; } void *printB(void *arg) { int i = 0; for(i = 1;i<11;i++){ sem_wait(&A_B); printf("B"); sem_post(&B_C); } return NULL; } void *printC(void *arg) { int i = 0; for(i = 1;i<11;i++){ sem_wait(&B_C); printf("C"); sem_post(&C_A); } return NULL; } ``` 在 `main()` 賦予初始值: ```c= pthread_t thread_A; pthread_t thread_B; pthread_t thread_C; sem_init(&A_B,0,0); sem_init(&B_C,0,0); sem_init(&C_A,0,1); pthread_create(&thread_A,NULL,printA,NULL); pthread_create(&thread_B,NULL,printB,NULL); pthread_create(&thread_C,NULL,printC,NULL); //... ``` 透過 Semaphore ,我們不止可以避免 Race condition ,更可以保證三個 Task 的執行順序。 > 原始碼連結: [Github](https://github.com/ianchen0119/Multi-thread/blob/master/semaphore/semaphore.c) 。 ### 與 Mutex lock 的異曲同工之妙 文章前面有提到, Semaphore 用於保持在 0 至指定最大值之間的一個計數值。 如果開發者將 Semaphore 設計成只會出現 0 和 1 兩種狀態,便稱為 **Binary semaphore**。 仔細想想, Binary semaphore 同樣確保一次只能有一個執行緒獲得共用資源的存取權,比較不同的是 Binary semaphore 不像 Mutex lock 有 Owner ship 的概念。因此在應用上, Binary semaphore 會更具彈性。 > 這裡的彈性是指,當一個 Thread 更改 Semaphore 的狀態,程式能夠允許另一個 Thread 對同一個 Semaphore 修改狀態。換句話說, Binary semaphore 可以讓我們造出一個能有它人解鎖的 Mutex lock 。 > 關於這個問題,網路上也有人設計相關實驗作探討,有興趣的讀者請參考該[連結](https://hackmd.io/@nKngvyhpQdGagg1V6GKLwA/Skh9Z2DpX?type=view)。 ## Deadlock 與 Livelock 使用鎖或是 Semaphore 確實可以避免 Race condition 的發生,不過,當開發者在設計機制時沒有考慮周全,就有可能讓程式邏輯**打結**。 ### Deadlock 簡單來說,當有兩個 Process 或是 Thread 需要彼此目前佔有的資源,而兩者卻又不願意釋放資源時,便會讓彼此的進度卡住,形成死結。又好比兩個人做交易,買家堅持賣家先出貨,賣家則堅持買家先付款,導致交易僵持,如果在電腦中出現類似的情況,就稱為 Deadlock 。 ### Deadlock 範例 考慮以下程式碼: ```c= // Source: https://stackoverflow.com/questions/27480125/simple-deadlock-example-using-pthread/50111909 pthread_mutex_t lock1, lock2; void *resource1(){ pthread_mutex_lock(&lock1); printf("Job started in resource1..\n"); sleep(2); printf("Trying to get resourc2\n"); pthread_mutex_lock(&lock2); printf("Aquired resourc2\n"); pthread_mutex_unlock(&lock2); printf("Job finished in resource1..\n"); pthread_mutex_unlock(&lock1); pthread_exit(NULL); } void *resource2(){ pthread_mutex_lock(&lock2); printf("Job started in resource2..\n"); sleep(2); printf("Trying to get resourc1\n"); pthread_mutex_lock(&lock1); printf("Aquired resourc1\n"); pthread_mutex_unlock(&lock1); printf("Job finished in resource2..\n"); pthread_mutex_unlock(&lock2); pthread_exit(NULL); } int main() { pthread_mutex_init(&lock1,NULL); pthread_mutex_init(&lock2,NULL); pthread_t t1,t2; pthread_create(&t1,NULL,resource1,NULL); pthread_create(&t2,NULL,resource2,NULL); pthread_join(t1,NULL); pthread_join(t2,NULL); return 0; } ``` 在上面的範例中,兩個 Task 都會嘗試 Lock 兩個互斥鎖,一個 Task 先上鎖 lock1 再上鎖 lock2 ,另一個則相反。當這兩個 Task 嘗試上獲得第二道鎖時,就會因為對方遲遲不交出資源而陷入 Deadlock 的狀況。 ### Deadlock 發動條件 要讓程式進入死結,必須天時地利人和: - No preemption: 占用系統資源的一個任務無法被強制停止。 - Hold and wait: 一個行程可以在等待時持有系統資源。 - Mutual exclusion: 系統資源只能同時分配給一個行程,無法在多個行程共享。 - Circular waiting: 多個行程互相持有其他行程所需要的資源。 要達成以上條件,程式才有可能進入 Deadlock ,作業系統在做排程設計時也會將其考慮進去,做出應對機制,像是: 讓任務是可以被作業系統強制結束的。 ### Starvation 當一個 Process 遲遲無法獲得所需資源,進入長期等待,這種情況就可以稱為 Starvation (飢餓)。作業系統為了避免飢餓/死結發生,除了上面提到的可搶斷式排程,也添加了老化的機制,也就是: 當一個 Process 久久沒有執行完成,作業系統便會逐步調高它的優先權,增加該 Process 完成的速度。 > 對排程演算法感興趣的話,可以參考[作業系統概念學習筆記 10 CPU 排程](https://www.itread01.com/content/1550575293.html)。 ### Livelock Livelock 不同於 Deadlock , Livelock 發生在彼此競爭但都願意讓步的情況,用生活上的例子就像是: 當我們走在行人道遇到對向的行人,兩個人都想讓路給對方卻又很剛好的站到同一邊,在電腦中,這樣看似有在執行卻毫無進展的情況就是 Livelock 。 ## Reference - [semaphore, mutex, spin lock](https://descent-incoming.blogspot.com/2015/11/semaphore-mutex-spin-lock.html) - Linux manual page - [Mutex, Semaphore, the difference, and Linux kernel](https://blog.louie.lu/2016/10/22/mutex-semaphore-the-difference-and-linux-kernel/) - [三、Queue 的應用(3) - Semaphore、Mutex的同步](https://ithelp.ithome.com.tw/articles/10219642) - [deadlock跟starvation教會我的人生哲學](https://antrash.pixnet.net/blog/post/91984155)