【Linux多线程】三个经典同步问题
在了解了《同步與互斥的區(qū)別》之后,我們來看看幾個經(jīng)典的線程同步的例子。相信通過具體場景可以讓我們學(xué)會分析和解決這類線程同步的問題,以便以后應(yīng)用在實際的項目中。
一、生產(chǎn)者-消費者問題
問題描述:
一組生產(chǎn)者進程和一組消費者進程共享一個初始為空、大小為 n 的緩沖區(qū),只有緩沖區(qū)沒滿時,生產(chǎn)者才能把消息放入到緩沖區(qū),否則必須等待;只有緩沖區(qū)不空時,消費者才能從中取出消息,否則必須等待。由于緩沖區(qū)是臨界資源,它只允許一個生產(chǎn)者放入消息,或者一個消費者從中取出消息。
分析:
關(guān)系分析:生產(chǎn)者和消費者對緩沖區(qū)互斥訪問是互斥關(guān)系,同時生產(chǎn)者和消費者又是一個相互協(xié)作的關(guān)系,只有生產(chǎn)者生產(chǎn)之后,消費者才能消費,它們也是同步關(guān)系。
整理思路:這里比較簡單,只有生產(chǎn)者和消費者兩個進程,且這兩個進程存在著互斥關(guān)系和同步關(guān)系。那么需要解決的是互斥和同步的PV操作的位置。
信號量設(shè)置:信號量mutex作為互斥信號量,用于控制互斥訪問緩沖池,初值為1;信號量full用于記錄當(dāng)前緩沖池中“滿”緩沖區(qū)數(shù),初值為 0;信號量empty用于記錄當(dāng)前緩沖池中“空”緩沖區(qū)數(shù),初值為n。
代碼示例:(semaphore類的封裝見下文)
#include<iostream> #include<unistd.h> // sleep #include<pthread.h> #include"semaphore.h" using namespace std; #define N 5semaphore mutex("/", 1); // 臨界區(qū)互斥信號量 semaphore empty("/home", N); // 記錄空緩沖區(qū)數(shù),初值為N semaphore full("/home/songlee",0); // 記錄滿緩沖區(qū)數(shù),初值為0 int buffer[N]; // 緩沖區(qū),大小為N int i=0; int j=0;void* producer(void* arg) {empty.P(); // empty減1mutex.P();buffer[i] = 10 + rand() % 90;printf("Producer %d write Buffer[%d]: %d\n",arg,i+1,buffer[i]);i = (i+1) % N;mutex.V();full.V(); // full加1 }void* consumer(void* arg) {full.P(); // full減1mutex.P();printf(" \033[1;31m");printf("Consumer %d read Buffer[%d]: %d\n",arg,j+1,buffer[j]);printf("\033[0m");j = (j+1) % N;mutex.V();empty.V(); // empty加1 }int main() {pthread_t id[10];// 開10個生產(chǎn)者線程,10個消費者線程for(int k=0; k<10; ++k)pthread_create(&id[k], NULL, producer, (void*)(k+1));for(int k=0; k<10; ++k)pthread_create(&id[k], NULL, consumer, (void*)(k+1));sleep(1);return 0; }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
編譯運行輸出結(jié)果:
Producer 1 write Buffer[1]: 83 Producer 2 write Buffer[2]: 26 Producer 3 write Buffer[3]: 37 Producer 5 write Buffer[4]: 35 Producer 4 write Buffer[5]: 33Consumer 1 read Buffer[1]: 83 Producer 6 write Buffer[1]: 35Consumer 2 read Buffer[2]: 26Consumer 3 read Buffer[3]: 37Consumer 4 read Buffer[4]: 35Consumer 5 read Buffer[5]: 33Consumer 6 read Buffer[1]: 35 Producer 7 write Buffer[2]: 56 Producer 8 write Buffer[3]: 22 Producer 10 write Buffer[4]: 79Consumer 9 read Buffer[2]: 56Consumer 10 read Buffer[3]: 22 Producer 9 write Buffer[5]: 11Consumer 7 read Buffer[4]: 79Consumer 8 read Buffer[5]: 11- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
二、讀者-寫者問題
問題描述:
有讀者和寫者兩組并發(fā)線程,共享一個文件,當(dāng)兩個或以上的讀線程同時訪問共享數(shù)據(jù)時不會產(chǎn)生副作用,但若某個寫線程和其他線程(讀線程或?qū)懢€程)同時訪問共享數(shù)據(jù)時則可能導(dǎo)致數(shù)據(jù)不一致的錯誤。因此要求:
- 允許多個讀者可以同時對文件執(zhí)行讀操作;
- 只允許一個寫者往文件中寫信息;
- 任一寫者在完成寫操作之前不允許其他讀者或?qū)懻吖ぷ?#xff1b;
- 寫者執(zhí)行寫操作前,應(yīng)讓已有的讀者和寫者全部退出。
分析:
關(guān)系分析:由題目分析可知,讀者和寫者是互斥的,寫者和寫者也是互斥的,而讀者和讀者不存在互斥問題。
整理思路:寫者是比較簡單的,它與任何線程互斥,用互斥信號量的 PV 操作即可解決。讀者的問題比較復(fù)雜,它必須實現(xiàn)與寫者的互斥,多個讀者還可以同時讀。所以,在這里用到了一個計數(shù)器,用它來判斷當(dāng)前是否有讀者讀文件。當(dāng)有讀者的時候?qū)懻呤菬o法寫文件的,此時讀者會一直占用文件,當(dāng)沒有讀者的時候?qū)懻卟趴梢詫懳募M瑫r,不同的讀者對計數(shù)器的訪問也應(yīng)該是互斥的。
信號量設(shè)置:首先設(shè)置一個計數(shù)器count,用來記錄當(dāng)前的讀者數(shù)量,初值為0;設(shè)置互斥信號量mutex,用于保護更新 count 變量時的互斥;設(shè)置互斥信號量rw用于保證讀者和寫者的互斥訪問。
代碼示例:
#include<iostream> #include<unistd.h> // sleep #include<pthread.h> #include"semaphore.h" using namespace std;int count = 0; // 記錄當(dāng)前的讀者數(shù)量 semaphore mutex("/",1); // 用于保護更新count變量時的互斥 semaphore rw("/home",1); // 用于保證讀者和寫者的互斥void* writer(void* arg) {rw.P(); // 互斥訪問共享文件printf(" Writer %d start writing...\n", arg);sleep(1);printf(" Writer %d finish writing...\n", arg);rw.V(); // 釋放共享文件 }void* reader(void* arg) {mutex.P(); // 互斥訪問count變量if(count == 0) // 當(dāng)?shù)谝粋€讀線程讀文件時rw.P(); // 阻止寫線程寫++count; // 讀者計數(shù)器加1mutex.V(); // 釋放count變量printf("Reader %d start reading...\n", arg);sleep(1);printf("Reader %d finish reading...\n", arg);mutex.P(); // 互斥訪問count變量--count; // 讀者計數(shù)器減1if(count == 0) // 當(dāng)最后一個讀線程讀完文件rw.V(); // 允許寫線程寫mutex.V(); // 釋放count變量 }int main() {pthread_t id[8]; // 開6個讀線程,2個寫線程pthread_create(&id[0], NULL, reader, (void*)1);pthread_create(&id[1], NULL, reader, (void*)2);pthread_create(&id[2], NULL, writer, (void*)1);pthread_create(&id[3], NULL, writer, (void*)2);pthread_create(&id[4], NULL, reader, (void*)3);pthread_create(&id[5], NULL ,reader, (void*)4);sleep(2);pthread_create(&id[6], NULL, reader, (void*)5);pthread_create(&id[7], NULL ,reader, (void*)6);sleep(4);return 0; }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
編譯運行的結(jié)果如下:
Reader 2 start reading... Reader 1 start reading... Reader 3 start reading... Reader 4 start reading... Reader 1 finish reading... Reader 2 finish reading... Reader 3 finish reading... Reader 4 finish reading...Writer 1 start writing...Writer 1 finish writing...Writer 2 start writing...Writer 2 finish writing... Reader 5 start reading... Reader 6 start reading... Reader 5 finish reading... Reader 6 finish reading...- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
三、哲學(xué)家進餐問題
問題描述:
一張圓桌上坐著 5 名哲學(xué)家,桌子上每兩個哲學(xué)家之間擺了一根筷子,桌子的中間是一碗米飯,如圖所示:
哲學(xué)家們傾注畢生精力用于思考和進餐,哲學(xué)家在思考時,并不影響他人。只有當(dāng)哲學(xué)家饑餓的時候,才試圖拿起左、右兩根筷子(一根一根拿起)。如果筷子已在他人手上,則需等待。饑餓的哲學(xué)家只有同時拿到了兩根筷子才可以開始進餐,當(dāng)進餐完畢后,放下筷子繼續(xù)思考。
分析:
關(guān)系分析:5名哲學(xué)家與左右鄰居對其中間筷子的訪問是互斥關(guān)系。
整理思路:顯然這里有 5 個線程,那么要如何讓一個哲學(xué)家拿到左右兩個筷子而不造成死鎖或饑餓現(xiàn)象?解決方法有兩個,一個是讓他們同時拿兩個筷子;二是對每個哲學(xué)家的動作制定規(guī)則,避免饑餓或死鎖現(xiàn)象的發(fā)生。
信號量設(shè)置:定義互斥信號量數(shù)組chopstick[5] = {1,1,1,1,1}用于對 5 根筷子的互斥訪問。
示例代碼:
semaphore chopstick[5] = {1,1,1,1,1} // 信號量數(shù)組 Pi() // i號哲學(xué)家的線程 {do{P(chopstick[i]); // 取左邊筷子P(chopstick[(i+1)%5]); // 取右邊筷子eat; // 進餐V(chopstick[i]); // 放回左邊筷子V(chopstick[(i+1)%5]); // 放回右邊筷子think; // 思考}while(1); }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
上面的偽代碼存在一個問題:當(dāng)五個哲學(xué)家都想要進餐,分別拿起他們左邊筷子的時候(都恰好執(zhí)行完P(guān)(chopstick[i])),筷子已經(jīng)被拿光了,等到他們再想拿右邊的筷子的時候,就全被阻塞了,這就出現(xiàn)了死鎖。
為了防止死鎖的發(fā)生,可以對哲學(xué)家線程施加一些限制條件,比如:
- 至多允許四個哲學(xué)家同時進餐;
- 僅當(dāng)一個哲學(xué)家左右兩邊的筷子都可用時才允許他抓起筷子;
- 對哲學(xué)家順序編號,要求奇數(shù)號哲學(xué)家先抓左邊的筷子,然后再抓他右邊的筷子,而偶數(shù)號哲學(xué)家剛好相反。
這里,我們采用第二種方法來改進上面的算法,即當(dāng)一個哲學(xué)家左右兩邊的筷子都可用時,才允許他抓起筷子。
#include<iostream> #include<vector> #include<unistd.h> // sleep #include<pthread.h> #include"semaphore.h" using namespace std;vector<semaphore*> chopstick; // 信號量數(shù)組 semaphore mutex("/", 1); // 設(shè)置取左右筷子的信號量 <-- 關(guān)鍵void* P1(void* arg) // 第1個哲學(xué)家線程 {mutex.P(); // 在取筷子前獲得互斥量chopstick[0]->P(); // 取左邊筷子chopstick[1]->P(); // 取右邊筷子mutex.V(); // 釋放取筷子的信號量printf("Philosopher 1 eat.\n");chopstick[0]->V(); // 放回左邊筷子chopstick[1]->V(); // 放回右邊筷子 }void* P2(void* arg) // 第2個哲學(xué)家線程 {mutex.P(); // 在取筷子前獲得互斥量chopstick[1]->P(); // 取左邊筷子chopstick[2]->P(); // 取右邊筷子mutex.V(); // 釋放取筷子的信號量printf("Philosopher 2 eat.\n");chopstick[1]->V(); // 放回左邊筷子chopstick[2]->V(); // 放回右邊筷子 }void* P3(void* arg) // 第3個哲學(xué)家線程 {mutex.P(); // 在取筷子前獲得互斥量chopstick[2]->P(); // 取左邊筷子chopstick[3]->P(); // 取右邊筷子mutex.V(); // 釋放取筷子的信號量printf("Philosopher 3 eat.\n");chopstick[2]->V(); // 放回左邊筷子chopstick[3]->V(); // 放回右邊筷子 }void* P4(void* arg) // 第4個哲學(xué)家線程 {mutex.P(); // 在取筷子前獲得互斥量chopstick[3]->P(); // 取左邊筷子chopstick[4]->P(); // 取右邊筷子mutex.V(); // 釋放取筷子的信號量printf("Philosopher 4 eat.\n");chopstick[3]->V(); // 放回左邊筷子chopstick[4]->V(); // 放回右邊筷子 }void* P5(void* arg) // 第5個哲學(xué)家線程 {mutex.P(); // 在取筷子前獲得互斥量chopstick[4]->P(); // 取左邊筷子chopstick[0]->P(); // 取右邊筷子mutex.V(); // 釋放取筷子的信號量printf("Philosopher 5 eat.\n");chopstick[4]->V(); // 放回左邊筷子chopstick[0]->V(); // 放回右邊筷子 }int main() {semaphore *sem1 = new semaphore("/home", 1);semaphore *sem2 = new semaphore("/home/songlee", 1);semaphore *sem3 = new semaphore("/home/songlee/java", 1);semaphore *sem4 = new semaphore("/home/songlee/ADT", 1);semaphore *sem5 = new semaphore("/home/songlee/Test", 1);chopstick.push_back(sem1);chopstick.push_back(sem2);chopstick.push_back(sem3);chopstick.push_back(sem4);chopstick.push_back(sem5);pthread_t id;pthread_create(&id, NULL, P1, NULL);pthread_create(&id, NULL, P2, NULL);pthread_create(&id, NULL, P3, NULL);pthread_create(&id, NULL, P4, NULL);pthread_create(&id, NULL, P5, NULL);sleep(1);delete sem1;delete sem2;delete sem3;delete sem4;delete sem5;return 0; }- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
編譯運行的結(jié)果如下:
Philosopher 2 eat. Philosopher 1 eat. Philosopher 3 eat. Philosopher 4 eat. Philosopher 5 eat.- 1
- 2
- 3
- 4
- 5
注意:創(chuàng)建信號量時的 路徑參數(shù) 請改成你的系統(tǒng)中存在的路徑!!!
附:semaphore類的封裝
上面的代碼中都使用了這個semaphore類,實現(xiàn)如下:
- semaphore.h
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- semaphore.cpp
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
在這里,要創(chuàng)建不同的信號量,必須傳遞不同的路徑參數(shù)(這樣獲取的 key 值才會不一樣)。
注意,本文的關(guān)注點并不在于 linux 下如何創(chuàng)建信號量以及如何封裝起來才更方便,而是通過幾個經(jīng)典的同步實例,了解在多線程環(huán)境下如何解決這類線程同步問題。
個人站點:http://songlee24.github.com
總結(jié)
以上是生活随笔為你收集整理的【Linux多线程】三个经典同步问题的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CORBA简介
- 下一篇: linux图形化应用程序快捷方式制作方法