天天看點

程序與線程(三)——程序/線程間通信在使用者空間中建立線程程序間通信

在使用者空間中建立線程

用庫函數實作線程(《現代作業系統》 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,則等待。

3、嚴格輪換法

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

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

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

模型概述:

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

問題:——可能會發生的競争條件——由于count的通路未加限制

信号量

概念:

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個緩沖塊視為共享資源,生産者寫人資料的緩沖塊成為消費者可用資源,而消費者讀出資料後的緩沖塊成為生産者的可用資源。為此,可設定三個信号量:full、empty和mutex。

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

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

mutex用于通路緩沖區時的互斥,初值是1。

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

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

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

讀者寫者

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

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

程式與線程(三)——程式/線程間通信在使用者空間中建立線程程式間通信

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

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

哲學家

程式與線程(三)——程式/線程間通信在使用者空間中建立線程程式間通信
程式與線程(三)——程式/線程間通信在使用者空間中建立線程程式間通信

分析:

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

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

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

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

程式與線程(三)——程式/線程間通信在使用者空間中建立線程程式間通信

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

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

程式與線程(三)——程式/線程間通信在使用者空間中建立線程程式間通信

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

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

程式與線程(三)——程式/線程間通信在使用者空間中建立線程程式間通信

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

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

a. 如果目前處理的哲學家處于饑餓狀态且兩側哲學家不在吃飯狀态,則目前哲學家通過 test()函數試圖進入吃飯狀态。 

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

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

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

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

許轉換到進餐狀态。 

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

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

但是該算法能夠實作對于任意多位哲學家的情況都能獲得最大的并行度,是以具有重要 的意義。