1. 程式人生 > >程序與執行緒(三)——程序/執行緒間通訊

程序與執行緒(三)——程序/執行緒間通訊

在使用者空間中建立執行緒

 

用庫函式實現執行緒(《現代作業系統》 P61)

#include<pthread.h>
#include<stdio.h>
#include<stdlib.h>

#define NUMBER_OF_THREADS 10

void *print_hello_world(void* tid){
   printf("hello world, greeting from thread %d\n", tid);
   pthread_exit(NULL);
}

int main(int argc, char *argv[]){
   pthread_t threads[NUMBER_OF_THREADS];
   int status=0;

   for(int i=0; i<NUMBER_OF_THREADS; i++){
      printf("Main hrer, Creating thread %d\n", i);
      status=pthread_create(&threads[i], NULL, print_hello_world, (void *)i);

      if(status !=0){
         printf("Oops.thread_creat returned error code %d\n", status);
         exit(-1);
      }
   }
   exit(NULL);
}
~             

在centos下對上述程式碼進行編譯:

g++ -lpthread -o 第一個執行緒建立的例子  第一個執行緒建立的例子.cpp 

生成可執行檔案,執行結果:

 

在使用者空間管理執行緒時,每個程序需要一個專用的執行緒表(thread table),用來跟蹤該程序中的執行緒。這個表與核心中的程序表類似。當一個執行緒轉換到就緒態或阻塞態時,線上程表中存放重啟該執行緒所需的資訊,與核心在程序表中存放程序的資訊完全一樣。

 

 

 

程序間通訊

 

競爭條件(race condition)(《現代作業系統》P67)

兩個或多個程序讀寫某些共享資料,而最後的結果取決於程序執行的精確時序

 

引起同步問題的因素:

1、時鐘中斷

當一個程序測試鎖的值為0時,此時發生一次時鐘中斷,CPU切換到另一個程序,另一個程序讀取到鎖的值為0,因此將鎖的值改為1並進入臨界區,再次發生時鐘中斷,CPU切換回來,剛剛讀取鎖的值為0的程序也將鎖的值賦值為1並進入臨界區。

 

2、訊號量丟失

當消費者從緩衝區中取出一個元素之後,count的值變為0,此時此時排程程式決定暫停消費者並啟動生產者。生產者想緩衝區中加入一個元素,此時生產者發現count的值變成了1,他推斷由於各個count為0,所以消費者一定在睡眠,於是生產者迪奧喲經wakeup來喚醒消費者。

但是此時消費者並沒有睡眠,因此wakeup訊號丟失。當消費者執行時,他上一次讀到的count值為0,因此進入睡眠。生產者遲早會填滿緩衝區,然後睡眠。這樣兩個程序都會永遠睡眠下去。

 

 

任何可能出錯的地方終將出錯。

 

臨界區(critical section)

互斥(mutual exclusion)——某種可以阻止多個程序同時讀寫共享資料的途徑。

 

避免競爭條件(race condition)的解決方案,需要滿足四個條件

1、任何兩個程序不能同時處於其臨界區

2、不應對CPU的速度和數量做任何假設

3、臨界區外執行的程序不得阻塞其他程序

4、不得使程序無限期等待進入臨界區

 

 

幾種實現互斥的方案

 

1、遮蔽中斷

程序進入臨界區之後立即遮蔽所有中斷,並在就要離開之前再開啟中斷。遮蔽中斷之後,時鐘中斷也被遮蔽。

弊端:

a、若一個程序遮蔽中斷之後不再開啟中斷,整個系統可能會因此終止。

b、遮蔽中斷僅對執行disable指令的那個cpu有效,其他CPU仍然可以繼續執行。

 

2、鎖變數

設定一把共享鎖,設定其初始值為0.當一個程序想進入臨界區,需要先測試這把鎖。如果鎖的值為0,則該程序將其設定為1並進入臨界區。若鎖的值已經為1,則等待。

弊端:

當一個程序測試鎖的值為0時,此時發生一次時鐘中斷,CPU切換到另一個程序,另一個程序讀取到鎖的值為0,因此將鎖的值改為1並進入臨界區,再次發生時鐘中斷,CPU切換回來,剛剛讀取鎖的值為0的程序也將鎖的值賦值為1並進入臨界區。

 

3、嚴格輪換法

整型變數turn的初始值為0,用於記錄哪一個程序進入臨界區,並檢查或更新共享記憶體。開始時,程序0檢查turn,發現其值為0,則進入臨界區,程序1發現turn的值為0,則進入忙等待(連續測試一個變數知道某個值出現為止,這種方法叫忙等待,用於忙等待的鎖叫自旋鎖)模式,等turn的值被程序0變成1之後,程序1才能進入臨界區。

弊端:

在一個程序比另一個程序慢很多的情況下,該方法不是一個好方法。程序會被一個臨界區之外的程序阻塞(情景:當程序0想進去臨界區時,turn的值還是1,而此時程序1在忙於臨界區之外的操作,程序0只能繼續忙等待)

 

 

 

生產著消費者(三個經典同步問題那裡還有更細緻的描述)

 

模型概述:

兩個程序共享一個公共固定大小的緩衝區,用一個變數count來跟蹤緩衝區中的元素個數。其中一個是生產者,他將資訊放入緩衝區,一個是消費者,他從緩衝區中取出資訊。當消費者發現count的值為0時,就進行睡眠等待;當生產者發現cout的值等於緩衝區大小時,則進行睡眠等待。

問題:——可能會發生的競爭條件——由於count的訪問未加限制

當消費者從緩衝區中取出一個元素之後,count的值變為0,此時此時排程程式決定暫停消費者並啟動生產者。生產者想緩衝區中加入一個元素,此時生產者發現count的值變成了1,他推斷由於各個count為0,所以消費者一定在睡眠,於是生產者迪奧喲經wakeup來喚醒消費者。

但是此時消費者並沒有睡眠,因此wakeup訊號丟失。當消費者執行時,他上一次讀到的count值為0,因此進入睡眠。生產者遲早會填滿緩衝區,然後睡眠。這樣兩個程序都會永遠睡眠下去。

 

 

 

訊號量

概念:

1、一個用於累計喚醒次數的整型變數

2、訊號量(semaphore)的資料結構為一個值和一個指標,指標指向等待該訊號量的下一個程序。訊號量的值與相應資源的使用情況有關。

當它的值大於0時,表示當前可用資源的數量;

當它的值小於0時,其絕對值表示等待使用該資源的程序個數。

注意,訊號量的值僅能由PV操作來改變。

 

訊號量S>=0,S表示可用資源的數量。執行一次P操作意味著請求分配一個單位資源,因此S的值減1;

當S<0,表示已經沒有可用資源,請求者必須等待別的程序釋放該類資源,它才能執行下去。而執行一個V操作意味著釋放一個單位資源,因此S的值加1;

若S<=0,表示有某些程序正在等待該資源,因此要喚醒一個等待狀態的程序,使之執行下去。

 

用處

用於實現同步、用於實現互斥

 

 

 

互斥量——訊號量的簡化版本——沒有訊號量的計數能力

 

適用於

管理共享資源或一小段程式碼;實現使用者空間執行緒包。

互斥量只有兩種狀態——解鎖和加鎖。

 

過程描述

當一個執行緒/程序需要訪問臨界區時,他呼叫mutex_lock。如果該互斥量當前是解鎖的(即臨界區可用),次呼叫成功,呼叫執行緒可以自由進入該臨界區。

 

如果互斥量已經加鎖,呼叫執行緒被阻塞,知道臨界區中的執行緒完成並呼叫mutex_unlock。如果多個執行緒被阻塞在該互斥量上,將隨機選擇一個執行緒並允許他獲得鎖。

 

Thread_yied之所在使用者空間中對執行緒排程的一個呼叫。因此mutex_lock和mutex_unlock都不需要任何核心呼叫。

 

 

 

Pthread

提供很多可以用來同步執行緒的函式。

基本機制

使用一個可以被鎖定和解鎖的互斥量保護每一個臨界區。該互斥鎖由程式設計師保證執行緒正確的使用它們。

條件變數——pthread的另一種同步機制

互斥量在允許火阻塞對臨界區的訪問上有用;

條件變數允許執行緒由於一些未達到的條件而阻塞。

 

生產者使用互斥量可以進行原子性檢查。但是當發生緩衝區已滿適,生產者需要一種方法阻塞自己並在以後喚醒,這便是條件變數該做的事了。

 

條件變數——互斥量模式

條件變數和互斥量經常一起使用。這種模式用於讓一個執行緒鎖住一個互斥量,然後當他不能獲得他期待的結果時等待一個條件變數。最後另一個執行緒會向他發起訊號,使他可以繼續執行。

(注意:條件變數不像訊號量,他不會存在記憶體中。如果將一個訊號量傳遞給一個沒有執行緒等待的條件變數,那麼這個訊號會丟失。程式設計師要小心使用避免丟失訊號)

 

使用了互斥量和條件變數的,只有一個快取的生產者消費者問題程式碼在阿里雲裡。去看吧,還是弄下來吧

  1 #include<stdio.h>
  2 #include<pthread.h>
  3 #define MAX 100000
  4 pthread_mutex_t the_mutex;
  5 pthread_cond_t condc, condp;
  6 int buffer=0;
  7 
  8 void * producer(void * ptr){
  9         for(int i=1 ; i<=MAX; i++){
 10                 pthread_mutex_lock(&the_mutex);
 11                 //自旋鎖,迴圈等待,直到條件出現
 12                 while(buffer!=0) pthread_cond_wait(&condp, &the_mutex);
 13                 buffer=i;
 14                 printf("第%d個問題\n", i);
 15                 pthread_cond_signal(&condc);
 16                 pthread_mutex_unlock(&the_mutex);
 17         }
 18         //exit(0):正常執行程式並退出程式;
 19         pthread_exit(0);
 20 }
 21 
 22 void * consumer(void * ptr){
 23         for(int i=1; i<=MAX; i++){
 24                 pthread_mutex_lock(&the_mutex);
 25                 while(buffer==0) pthread_cond_wait(&condc, &the_mutex);
 26                 buffer=0;
 27                 printf("當然啦!\n");
 28                 //喚醒生產者
 29                 pthread_cond_signal(&condp);
 30                 pthread_mutex_unlock(&the_mutex);
 31         }
 32         pthread_exit(0);
 33 }
 34 
 35 int main(int argc, char *argv){
 36         pthread_t pro, con;
 37         pthread_mutex_init(&the_mutex, 0);
 38         pthread_cond_init(&condc, 0);
 39         pthread_cond_init(&condp, 0);
 40         pthread_create(&con, 0, consumer, 0);
 41         pthread_create(&pro, 0, producer, 0);
 42         pthread_join(pro, 0);
 43         pthread_join(con, 0);
 44         pthread_cond_destroy(&condc);
 45         pthread_cond_destroy(&condp);
 46         pthread_mutex_destroy(&the_mutex);
 47 }

 

 

 

原子操作

為了確保訊號量可以正常工作,要採用一種不可分割的形式去實現它(原子操作:指一組相關聯的操作要麼都不間斷的執行,要麼都不執行)。保證一旦一個訊號量操作開始,則在該操作完成或阻塞之前,其他程序均不允許訪問該訊號量。

 

 

 

PV操作

PV操作作為系統呼叫實現,執行以下動作時需要暫時遮蔽全部中斷:測試訊號量更新訊號量以及在需要時使某個程序睡眠

PV操作由P操作原語和V操作原語組成(原語是不可中斷的過程),對訊號量進行操作,具體定義如下:

P(S):①將訊號量S的值減1,即S=S-1;

             ②如果S<=0,則該程序繼續執行;否則該程序置為等待狀態,排入等待佇列。

V(S):①將訊號量S的值加1,即S=S+1;

             ②如果S>0,則該程序繼續執行;否則釋放佇列中第一個等待訊號量的程序。

p操作(wait):申請一個單位資源,程序進入

v操作(signal):釋放一個單位資源,程序出來

 

 

 

 

三個經典的同步問題

 

生產著消費者

Mutex用於互斥,它用於保證任意時刻只有一個進城讀寫緩衝區和相關變數。

 

我們可把共享緩衝區中的n個緩衝塊視為共享資源,生產者寫人資料的緩衝塊成為消費者可用資源,而消費者讀出資料後的緩衝塊成為生產者的可用資源。為此,可設定三個訊號量:fullemptymutex

其中:full表示有資料的緩衝塊數目,初值是0;

empty表示空的緩衝塊數初值是n;

mutex用於訪問緩衝區時的互斥,初值是1。

實際上,full和empty間存在如下關係:full + empty = N

 

注意:這裡每個程序中各個P操作的次序是重要的(就是上面不能先申請mutex)。

各程序必須先檢查自己對應的資源數在確信有可用資源後再申請對整個緩衝區的互斥操作;否則,先申請對整個緩衝區的互斥操後申請自己對應的緩衝塊資源,就可能死鎖。出現死鎖的條件是,申請到對整個緩衝區的互斥操作後,才發現自己對應的緩衝塊資源,這時已不可能放棄對整個緩衝區的佔用。如果採用AND訊號量集,相應的進入區和退出區都很簡單。

 

 

讀者寫者

讀者一寫者問題(readers-writersproblem)是指多個程序對一個共享資源進行讀寫操作的問題。

假設“讀者”程序可對共享資源進行讀操作,“寫者”程序可對共享資源進行寫操作;任一時刻“寫者”最多隻允許一個,而“讀者”則允許多個。即對共享資源的讀寫操作限制關係包括:“讀—寫,互斥、“寫一寫”互斥和“讀—讀”允許

我們可認為寫者之間、寫者與第一個讀者之間要對共享資源進行互斥訪問,而後續讀者不需要互斥訪問。

為此,可設定兩個訊號量WmutexRmutex和一個公共變數Rcount。其中:Wmutex表示“允許寫”,初值是1;公共變數Rcount表示“正在讀”的程序數,初值是0;Rmutex表示對Rcount的互斥操作,初值是1。

 

 

哲學家 

分析:

假如所有的哲學家都同時拿起左側筷子,看到右側筷子不可用,又都放下左側筷子, 等一會兒,又同時拿起左側筷子,如此這般,永遠重複。對於這種情況,即所有的程式都在 無限期地執行,但是都無法取得任何進展,即出現飢餓,所有哲學家都吃不上飯。

描述一種沒有人餓死(永遠拿不到筷子)演算法

A.原理:至多隻允許四個哲學家同時進餐,

以下將room 作為訊號量,只允 許4 個哲學家同時進入餐廳就餐,這樣就能保證至少有一個哲學家可以就餐,而申請進入 餐廳的哲學家進入room 的等待佇列,根據FIFO 的原則,總會進入到餐廳就餐,因此不會 出現餓死和死鎖的現象。 

B.原理:僅當哲學家的左右兩支筷子都可用時,才允許他拿起筷子進餐

利用訊號量的保護機制實現。通過訊號量mutexeat()之前的取左側和右側筷 子的操作進行保護,使之成為一個原子操作,這樣可以防止死鎖的出現

C. 原理:規定奇數號的哲學家先拿起他左邊的筷子,然後再去拿他右邊的筷子;而偶數號 的哲學家則相反.

按此規定,將是1,2號哲學家競爭1號筷子,3,4號哲學家競爭3號筷子.即 五個哲學家都競爭奇數號筷子,獲得後,再去競爭偶數號筷子,最後總會有一個哲學家能獲 得兩支筷子而進餐。而申請不到的哲學家進入阻塞等待佇列,根FIFO原則,則先申請的哲 學家會較先可以吃飯,因此不會出現餓死的哲學家。 

D.利用管程機制實現(最終該實現是失敗的,見以下分析): 

原理:不是對每隻筷子設定訊號量,而是對每個哲學家設定訊號量。test()函式有以下作 用: 

a. 如果當前處理的哲學家處於飢餓狀態且兩側哲學家不在吃飯狀態,則當前哲學家通過 test()函式試圖進入吃飯狀態。 

b. 如果通過test()進入吃飯狀態不成功,那麼當前哲學家就在該訊號量阻塞等待,直到 

其他的哲學家程序通過test()將該哲學家的狀態設定為EATING。 

c. 當一個哲學家程序呼叫put_forks()放下筷子的時候,會通過test()測試它的鄰居, 如果鄰居處於飢餓狀態,且該鄰居的鄰居不在吃飯狀態,則該鄰居進入吃飯狀態。 

由上所述,該演算法不會出現死鎖,因為一個哲學家只有在兩個鄰座都不在進餐時,才允 

許轉換到進餐狀態。 

該演算法會出現某個哲學家適終無法吃飯的情況,即當該哲學家的左右兩個哲學家交替 

處在吃飯的狀態的時候,則該哲學家始終無法進入吃飯的狀態,因此不滿足題目的要求。 

但是該演算法能夠實現對於任意多位哲學家的情況都能獲得最大的並行度,因此具有重要 的意義。