簡介
程式同步是一個作業系統級別的概念,是在多道程式的環境下,存在著不同的制約關係,為了協調這種互相制約的關係,實現資源共享和程式協作,從而避免程式之間的衝突,引入了程式同步。
臨界資源
在作業系統中,程式是佔有資源的最小單位(執行緒可以訪問其所在程式內的所有資源,但執行緒本身並不佔有資源或僅僅佔有一點必須資源)。但對於某些資源來說,其在同一時間只能被一個程式所佔用。這些一次只能被一個程式所佔用的資源就是所謂的臨界資源。典型的臨界資源比如物理上的印表機,或是存在硬碟或記憶體中被多個程式所共享的一些變數和資料等(如果這類資源不被看成臨界資源加以保護,那麼很有可能造成丟資料的問題)。
對於臨界資源的訪問,必須是互訴進行。也就是當臨界資源被佔用時,另一個申請臨界資源的程式會被阻塞,直到其所申請的臨界資源被釋放。而程式內訪問臨界資源的程式碼被成為臨界區。
對於臨界區的訪問過程分為四個部分:
1.進入區:檢視臨界區是否可訪問,如果可以訪問,則轉到步驟二,否則程式會被阻塞
2.臨界區:在臨界區做操作
3.退出區:清除臨界區被佔用的標誌
4.剩餘區:程式與臨界區不相關部分的程式碼
程式間同步和互訴的概念
程式同步
程式同步也是程式之間直接的制約關係,是為完成某種任務而建立的兩個或多個執行緒,這個執行緒需要在某些位置上協調他們的工作次序而等待、傳遞資訊所產生的制約關係。程式間的直接制約關係來源於他們之間的合作。
比如說程式A需要從緩衝區讀取程式B產生的資訊,當緩衝區為空時,程式B因為讀取不到資訊而被阻塞。而當程式A產生資訊放入緩衝區時,程式B才會被喚醒。概念如圖1所示。
圖1.程式之間的同步
用C#程式碼模擬程式之間的同步如程式碼1所示。
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 |
class ProcessSyn { private static Mutex mut = new Mutex(); static void Main() { Console.WriteLine("程式1執行完了程式2才能執行......."); Thread Thread1 = new Thread(new ThreadStart(Proc1)); Thread Thread2 = new Thread(new ThreadStart(Proc2)); Thread1.Start(); Thread2.Start(); Console.ReadKey(); } private static void Proc1() { mut.WaitOne(); Console.WriteLine("執行緒1執行操作...."); Thread.Sleep(3000); mut.ReleaseMutex();//V操作 } private static void Proc2() { mut.WaitOne();//P操作 Console.WriteLine("執行緒2執行操作...."); mut.WaitOne(); } } |
程式碼1.C#模擬程式之間的同步
執行結果如圖2所示。
圖2.執行結果
程式互斥
程式互斥是程式之間的間接制約關係。當一個程式進入臨界區使用臨界資源時,另一個程式必須等待。只有當使用臨界資源的程式退出臨界區後,這個程式才會解除阻塞狀態。
比如程式B需要訪問印表機,但此時程式A佔有了印表機,程式B會被阻塞,直到程式A釋放了印表機資源,程式B才可以繼續執行。概念如圖3所示。
圖3.程式之間的互斥
用C#模擬程式之間的互斥,這裡我啟動了5個執行緒,但同一時間內只有一個執行緒能對臨界資源進行訪問。如程式碼2所示。
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 |
class ProcessMutex { private static Mutex mut = new Mutex(); private const int numThreads = 5; static void Main() { for (int i = 0; i new Thread(new ThreadStart(UseResource)); myThread.Name = String.Format("執行緒{0}", i + 1); myThread.Start(); } Console.ReadKey(); } //同步 private static void UseResource() { // 相當於P操作 mut.WaitOne(); /*下面程式碼是執行緒真正的工作*/ Console.WriteLine("{0}已經進入臨界區", Thread.CurrentThread.Name); Random r = new Random(); int rNum = r.Next(2000); Console.WriteLine("{0}執行操作,執行時間為{1}ms", Thread.CurrentThread.Name,rNum); Thread.Sleep(rNum); Console.WriteLine("{0}已經離開臨界區rn", Thread.CurrentThread.Name); /*執行緒工作結束*/ // 相當於V操作 mut.ReleaseMutex(); } //互斥 } |
程式碼2.C#模擬程式之間的互斥
執行結果如圖4所示。
圖4.C#模擬程式互斥
實現臨界區互斥的基本方法
硬體實現方法
通過硬體實現臨界區最簡單的辦法就是關CPU的中斷。從計算機原理我們知道,CPU進行程式切換是需要通過中斷來進行。如果遮蔽了中斷那麼就可以保證當前程式順利的將臨界區程式碼執行完,從而實現了互斥。這個辦法的步驟就是:遮蔽中斷–執行臨界區–開中斷。但這樣做並不好,這大大限制了處理器交替執行任務的能力。並且將關中斷的許可權交給使用者程式碼,那麼如果使用者程式碼遮蔽了中斷後不再開,那系統豈不是跪了?
還有硬體的指令實現方式,這個方式和接下來要說的訊號量方式如出一轍。但是通過硬體來實現,這裡就不細說了。
訊號量實現方式
這也是我們比較熟悉P V操作。通過設定一個表示資源個數的訊號量S,通過對訊號量S的P和V操作來實現程式的的互斥。
P和V操作分別來自荷蘭語Passeren和Vrijgeven,分別表示佔有和釋放。P V操作是作業系統的原語,意味著具有原子性。
P操作首先減少訊號量,表示有一個程式將佔用或等待資源,然後檢測S是否小於0,如果小於0則阻塞,如果大於0則佔有資源進行執行。
V操作是和P操作相反的操作,首先增加訊號量,表示佔用或等待資源的程式減少了1個。然後檢測S是否小於0,如果小於0則喚醒等待使用S資源的其它程式。
前面我們C#模擬程式的同步和互斥其實算是訊號量進行實現的。
一些經典利用訊號量實現同步的問題
生產者–消費者問題
問題描述:生產者-消費者問題是一個經典的程式同步問題,該問題最早由Dijkstra提出,用以演示他提出的訊號量機制。本作業要求設計在同一個程式地址空間內執行的兩個執行緒。生產者執行緒生產物品,然後將物品放置在一個空緩衝區中供消費者執行緒消費。消費者執行緒從緩衝區中獲得物品,然後釋放緩衝區。當生產者執行緒生產物品時,如果沒有空緩衝區可用,那麼生產者執行緒必須等待消費者執行緒釋放出一個空緩衝區。當消費者執行緒消費物品時,如果沒有滿的緩衝區,那麼消費者執行緒將被阻塞,直到新的物品被生產出來
這裡生產者和消費者是既同步又互斥的關係,首先只有生產者生產了,消費著才能消費,這裡是同步的關係。但他們對於臨界區的訪問又是互斥的關係。因此需要三個訊號量empty和full用於同步緩衝區,而mut變數用於在訪問緩衝區時是互斥的。
利用C#模擬生產者和消費者的關係如程式碼3所示。
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 |
class ProducerAndCustomer { //臨界區訊號量 private static Mutex mut = new Mutex(); private static Semaphore empty = new Semaphore(5, 5);//空閒緩衝區 private static Semaphore full = new Semaphore(0, 5); //生產者-消費者模擬 static void Main() { Console.WriteLine("生產者消費者模擬......"); for (int i = 1; i new Thread(new ThreadStart(Producer)); Thread Thread2 = new Thread(new ThreadStart(Customer)); Thread1.Name = String.Format("生產者執行緒{0}", i); Thread2.Name = String.Format("消費者執行緒{0}", i); Thread1.Start(); Thread2.Start(); } Console.ReadKey(); } private static void Producer() { Console.WriteLine("{0}已經啟動",Thread.CurrentThread.Name); empty.WaitOne();//對empty進行P操作 mut.WaitOne();//對mut進行P操作 Console.WriteLine("{0}放入資料到臨界區", Thread.CurrentThread.Name); Thread.Sleep(1000); mut.ReleaseMutex();//對mut進行V操作 full.Release();//對full進行V操作 } private static void Customer() { Console.WriteLine("{0}已經啟動", Thread.CurrentThread.Name); Thread.Sleep(12000); full.WaitOne();//對full進行P操作 mut.WaitOne();//對mut進行P操作 Console.WriteLine("{0}讀取臨界區", Thread.CurrentThread.Name); mut.ReleaseMutex();//對mut進行V操作 empty.Release();//對empty進行V操作 } } |
程式碼3.使用C#模擬生產者和消費者的關係
執行結果如圖5所示。
圖5.生產者消費者C#模擬結果
讀者–寫者問題
問題描述:
一個資料檔案或記錄,統稱資料物件,可被多個程式共享,其中有些程式只要求讀稱為”讀者”,而另一些程式要求寫或修改稱為”寫者”。
規定:允許多個讀者同時讀一個共享物件,但禁止讀者、寫者同時訪問一個共享物件,也禁止多個寫者訪問一個共享物件,否則將違反Bernstein併發執行條件。
通過描述可以分析,這裡的讀者和寫者是互斥的,而寫者和寫者也是互斥的,但讀者之間並不互斥。
由此我們可以設定3個變數,一個用來統計讀者的數量,另外兩個分別用於對讀者數量讀寫的互斥,讀者和讀者寫者和寫者的互斥。如程式碼4所示。
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 |
class ReaderAndWriter { private static Mutex mut = new Mutex();//用於保護讀者數量的互斥訊號量 private static Mutex rw = new Mutex();//保證讀者寫者互斥的訊號量 static int count = 0;//讀者數量 static void Main() { Console.WriteLine("讀者寫者模擬......"); for (int i = 1; i new Thread(new ThreadStart(Reader)); Thread1.Name = String.Format("讀者執行緒{0}", i); Thread1.Start(); } Thread Thread2 = new Thread(new ThreadStart(writer)); Thread2.Name = String.Format("寫者執行緒"); Thread2.Start(); Console.ReadKey(); } private static void Reader() { mut.WaitOne(); if (count == 0) { rw.WaitOne(); } count++; mut.ReleaseMutex(); Thread.Sleep(new Random().Next(2000));//讀取耗時1S Console.WriteLine("讀取完畢"); mut.WaitOne(); count--; mut.ReleaseMutex(); if (count == 0) { rw.ReleaseMutex(); } } private static void writer() { rw.WaitOne(); Console.WriteLine("寫入資料"); rw.ReleaseMutex(); } |
程式碼4.C#模擬讀者和寫者問題
執行結果如圖6所示。
圖6.讀者寫者的執行結果 哲學家進餐問題 問題描述: 有五個哲學家,他們的生活方式是交替地進行思考和進餐。哲學家們公用一張圓桌,周圍放有五把椅子,每人坐一把。在圓桌上有五個碗和五根筷子,當一個哲學家思考時,他不與其他人交談,飢餓時便試圖取用其左、右最靠近他的筷子,但他可能一根都拿不到。只有在他拿到兩根筷子時,方能進餐,進餐完後,放下筷子又繼續思考。
圖7.哲學家進餐問題
根據問題描述,五個哲學家分別可以看作是五個程式。五隻筷子分別看作是五個資源。只有當哲學家分別擁有左右的資源時,才得以進餐。如果不指定規則,當每個哲學家手中只拿了一隻筷子時會造成死鎖,從而五個哲學家都因為吃不到飯而餓死。因此我們的策略是讓哲學家同時拿起兩隻筷子。因此我們需要對每個資源設定一個訊號量,此外,還需要使得哲學家同時拿起兩隻筷子而設定一個互斥訊號量,如程式碼5所示。
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 |
class philosopher { private static int[] chopstick=new int[5];//分別代表哲學家的5只筷子 private static Mutex eat = new Mutex();//用於保證哲學家同時拿起兩雙筷子 static void Main() { //初始設定所有筷子可用 for (int k = 1; k //每個哲學家輪流進餐一次 for(int i=1;inew Thread(new ThreadStart(Philosophers)); Thread1.Name = i.ToString(); Thread1.Start(); } Console.ReadKey(); } private static void Philosophers() { //如果筷子不可用,則等待2秒 while (chopstick[int.Parse(Thread.CurrentThread.Name)-1] !=1 || chopstick[(int.Parse(Thread.CurrentThread.Name))%4]!=1) { Console.WriteLine("哲學家{0}正在等待", Thread.CurrentThread.Name); Thread.Sleep(2000); } eat.WaitOne(); //同時拿起兩雙筷子 chopstick[int.Parse(Thread.CurrentThread.Name)-1] = 0; chopstick[(int.Parse(Thread.CurrentThread.Name)) % 4] = 0; eat.ReleaseMutex(); Thread.Sleep(1000); Console.WriteLine("哲學家{0}正在用餐...",Thread.CurrentThread.Name); //用餐完畢後放下筷子 chopstick[int.Parse(Thread.CurrentThread.Name)-1] = 1; chopstick[(int.Parse(Thread.CurrentThread.Name)) % 4] = 1; Console.WriteLine("哲學家{0}用餐完畢,繼續思考", Thread.CurrentThread.Name); } } |
程式碼5.C#模擬哲學家用餐問題
執行結果如圖7所示。
圖8.哲學家問題執行結果
總結
本文介紹了程式的同步和互斥的概念,臨界區的概念,以及實現程式同步互斥的方式,並解決了3種實現同步的經典問題,並給出了相應的C#模擬程式碼。作業系統對於程式的管理是是計算機程式設計的基礎之一,因此掌握這個概念會使你的內功更上一層:-D