基於管程的多線程程序

注:本文主要參考自<<現代操作系統>>2.3.7節

互斥量與條件變量一文中,我們使用互斥量與條件變量解決了多線程生產者與消費者問題.其中互斥量主要用於確保生產者與消費者不會同時訪問緩衝區,條件變量則用於確保線程僅在其執行必要條件滿足時纔會執行.本文,我們將介紹一種與該方法一脈相承的另一種技術:管程.

管程

首先,應當說明,管程屬於編程語言的範疇.是爲了方便程序員編程,而爲編程語言添加的新特性.因此,並不是所有的編程語言都支持管程這一特性.爲了支持管程,編程語言的編譯器需要做一些額外的工作.由於管程是在編譯器層面支持的,因此相對於程序員使用一些更低級的特性(如信號量)進程編程,使用管程進行多線程編程能夠降低出錯概率,因爲一些容易出錯的細節被編譯器封裝了起來.

一個管程是一個由過程、變量及數據結構等組成的一個集合,它們組成一個特殊的模塊或軟件包。進程可在任何需要的時候調用管程中的過程,一種但它們不能在管程之外聲明的過程中直接訪問管程內的數據結構。

管程有一個很重要的特性,即任一時刻管程中只能有一個活躍進程,這一特性使管程能有效地完成互斥。僅僅提供互斥還不夠,我們還需要類似條件變量的特性.因此,管程也提供wait()notify()等價的操作.程序員通常不用關心編譯器如何實現管程的互斥特性,僅需將多個線程對臨界區的操作放進管程中即可.

下圖展示了基於管程的生產者,消費者多線程程序類Pascal語言.
在這裏插入圖片描述
生產者對緩衝區的操作在insert方法中實現,消費者對緩衝區的操作在remove方法中實現,爲了確保對緩衝區的互斥操作,將insert與remove方法放入管程中即可.條件變量fullempty用於保證生產者進程與消費者進程僅在滿足執行條件時纔會執行.

注意到,由於管程的互斥特性,因此我們能夠確保條件變量在執行signal操作前一定已經執行了wait操作.因爲它不會在執行if count == 0 then wait(empty)這條語句時發生時鐘中斷.這與我們在互斥量,條件變量最後一節的討論相一致.

使用java語言管程解決奪進程生產者與消費者問題

java語言提供了對管程特性的支持,並支持用戶級線程.通過在類的方法前添加synchronized關鍵字可以將當前方法添加到管程中,因此在同一時刻,最多只能有一個線程在執行帶有synchronized關鍵字的方法.然而,java不支持內嵌條件變量,僅提供了wait()notify()兩個方法.因此無法像上例僞代碼中那樣分別定義emptyfull條件變量.

public class ProducerConsumer {
	static final int N = 100;// 定義緩衝區大小的常量
	static Producer p = new Producer();// 初始化一個新的生產者線程
	static Consumer c = new Consumer();// 初始化一個新的消費者線程
	static Our_monitor mon = new Our_monitor();// 初始化一個新的管程

	public static void main(String[] args) {
		p.start();// 生產者啓動
		c.start();// 消費者啓動
	}

	static class Producer extends Thread {
		public void run() {// 線程運行主代碼
			int item;
			while (true) {// 生產者循環
				item = produce_item();
				mon.insert(item);
			}
		}

		private int produce_item() {
			System.out.println("生產了1個");
			try {
				Thread.sleep(500);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			return 1;
		}// 實際生產
	}

	static class Consumer extends Thread {
		public void run() {// 線程運行主代碼
			int item;
			while (true) {// 消費者循環
				item = mon.remove();
				consume_item(item);
			}
		}

		private void consume_item(int item) {
			try {
				Thread.sleep(10);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("消費了1個");
		}// 實際消費
	}

	static class Our_monitor {// 管程
		private int buffer[] = new int[N];
		private int count = 0, lo = 0, hi = 0;// 計數器和索引

		public synchronized void insert(int val) {
			if (count == N)
				go_to_sleep();// 若緩衝區滿,則進入睡眠
			buffer[hi] = val;// 向緩衝區中插入一個新的數據項
			hi = (hi + 1) % N;// 設置下一個數據項的槽
			count = count + 1;// 緩衝區的數據項又多了一項
			if (count == 1)
				notify();// 如果消費者在休眠,則將其喚醒
		}

		public synchronized int remove() {
			int val;
			if (count == 0)
				go_to_sleep();// 如果緩衝區空,進入休眠
			val = buffer[lo];// 從緩衝區中取出一個數據項
			lo = (lo + 1) % N;// 設置待取數據項的槽
			count = count - 1;// 緩衝區的數據數目減少1
			if (count == N - 1)
				notify();// 如果生產者正在休眠,則將其喚醒
			return val;
		}

		private void go_to_sleep() {
			try {
				wait();
			} catch (InterruptedException exc) {
			}
			;
		}
	}
}

與管程和信號量有關的另一個問題是,這些機制都是設計用來解決訪問公共內存的一個或多個CPU上的互斥問題的。通過將信號量放在共享內存中並用TSL或XCHG指令來保護它們,可以避免競爭。如果一個分佈式系統具有多個CPU,並且每個CPU擁有自己的私有內存,它們通過一個局域網相連,那麼這些原語將失效。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章