閱讀382 返回首頁    go 阿裏雲 go 技術社區[雲棲]


任務取消(Cancellation)

當某個線程中的活動執行失敗或想改變運行意圖,也許就有必要或想要在其它線程中取消這個線程的活動,而不管這個線程正在做什麼。取消會給運行中的線程帶來 一些無法預料的失敗情況。取消操作異步特性相關的設計技巧,讓人想起了因係統崩潰和連接斷開任何時候都有可能失敗的分布式係統的那些技巧。並發程序還要確 保多線程共享的對象的狀態一致性。

在大多數多線程程序中,取消任務(Cancellation)是普遍存在的,常見於:

  • 幾乎所有與GUI中取消按鈕相關的活動。
  • 多媒體演示(如動畫循環)中的正常終止活動。
  • 線程中生成的結果不再需要。例如使用多個線程搜索數據庫,隻要某個線程返回了結果,其它的都可以取消掉。
  • 由於一組活動中的一或多個遇到意外錯誤或異常導致整組活動無法繼續。

腳注:在並發編程中兩個l的cancellation最常見。譯者注:英語”取消”有兩種寫法cancelation和cancellation


中斷(Interruption)

實現取消任務的最佳技術是使用線程的中斷狀態,這個狀態由Thread.interrupt設置,可被Thread.isInterrupted檢測到,通過Thread.interrupted清除,有時候拋出InterruptedException異常來響應。

腳注:JDK1.0不支持中斷機製。版本間政策與機製(policies and mechanisms)的更新說明對任務取消支持中的不規則行為做出了說明

線程中斷起著請求取消活動的作用。但無法阻止有人將其用作它途,而用來作取消操作是預期的用途。基於中斷的任務取消依賴於取消者和被取消者間的一個協議以 確保跨多線程使用的對象在被取消線程終止的時候不被損壞。大部分(理想情況下是所有的)java.*包中的類都遵守這個協議。

幾乎在所有的情況下,取消一個與線程有關係的活動都應當終止對應的線程。但中斷機製不會強製線程立馬終止。這就給任何被中斷的線程一個在終止前做些清理操作的機會,但也給代碼強加了及時檢查中斷狀態以及采取合適操作的職責。

延遲甚至忽略任務取消的請求給寫出良好響應性且非常健壯的代碼提供了途徑。因為不會直接將線程中斷掉,所以很難或不可能撤銷的動作的前麵可以作為一個安全點,然後在此安全點檢查中斷狀態。響應中斷大部分可選的方式在§3.1.1中有討論:

繼續執行(忽略或清除了中斷)可能適用於那些不打算終止的線程;例如,那些對於程序基本功能不可或缺的數據庫管理服務。一旦遇到中斷,可中止這些特殊的任 務,然後允許線程繼續執行其它任務。然而,即使在這裏,將中斷的線程替換成一個處於初始狀態的新啟動的線程會更易於管理。

突然終止(比如拋出錯誤)一般適用於提供獨立服務、除了run方法中finally子句外無需其它清理操作的線程。但是,當線程執行的服務被其它線程依賴時(見§4.3),就應當以某種形式通知這些依賴的線程或設置狀態指示。(異常本身不會自動在線程間傳播)

線程中使用的對象被其它線程依賴時必須使用回滾或前滾技術。

在某種程度上,可以通過決定多久用Thread.currentThread().isInterrupted()來檢查中斷狀態以控製代碼對中斷的響應 靈敏性。中斷狀態檢查不需要太頻繁以免影響程序效率。例如,如果需要取消的活動包含大約10000條指令,每10000條指令做一次取消檢查,那麼從取消 請求到關閉平均會耗費15000條指令。隻要活動繼續運行沒有什麼實際的危害,這個數量級可以滿足大部分應用的需要。通常,這個理由可以讓你將中斷檢測代 碼僅放到既方便檢測又是重要的程序點。在性能關鍵型應用中,也許值得構建一個分析模型或收集經驗值來準確地決定響應性與吞吐量間的最佳權衡(參 見§4.4.1.7)。

Object.wait、Thread.join、Thread.sleep以及它們衍生出的方法都會自動檢測中斷。這些方法一旦中斷就會拋出InterruptedException來中止,然後讓線程蘇醒並執行與活動取消相關的代碼。

按照慣例,應當在拋出InterruptedException時清除中斷狀態。有時候有必要這樣做來支持一些清理工作,但這也可能是錯誤與混亂之源。當 處理完InterruptedException後想要傳播中斷狀態,必須要麼重新拋出捕獲的InterruptedException,要麼通過 Thread.currentThread().interrupt()重新設置中斷狀態。如果你的代碼調用了其它未正確維持中斷狀態的代碼(例如,忽略 InterruptedException又不重設狀態),可以能通過維持一個字段來規避問題,這個字段用於保存活動取消的標識,在調用 interrupt的時候設置該字段,從有問題的調用中返回時檢查該字段。

有兩種情況線程會保持休眠而無法檢測中斷狀態或接收InterruptedException:在同步塊中和在IO中阻塞時。線程在等待同步方法或同步塊 的鎖時不會對中斷有響應。但是,如§2.5中討論的,當需要大幅降低在活動取消期間被卡在鎖等待中的幾率,可以使用lock工具類。使用lock類的代碼 阻塞僅是為了訪問鎖對象本身,而不是這些鎖所保護的代碼。這些阻塞的耗時天生就很短(盡管時間不能嚴格保證)。

IO和資源撤銷(IO and resource revocation)

一些IO支持類(尤其是java.net.Socket及其相關類)提供了在讀操作阻塞的時候能夠超時的可選途徑,在這種情況下就可以在超時後檢測中斷。 java.io中的其它類采用了另一種方式——一種特殊形式的資源撤銷。如果某個線程在一個IO對象s(如InputStream)上執行 s.close(),那麼任何其它嚐試使用s的線程將收到一個IOException。IO關閉會影響所有使用關閉了的IO對象的線程,會導致IO對象不 可用。如有必要,可以創建一個新IO對象來替代關閉了的IO對象。

這與其它資源撤銷的用途密切相關(如為了安全目的)。該策略也會保護應用免讓共享的IO對象因其它使用了此IO對象的線程被取消而自動變得不可用。大部分 java.io中的類不會也不能在出現IO異常時清除失敗狀態。例如,如果在StreamTokenizer或ObjectInputStream操作中 間出現了一個底層IO異常,沒有一個實用的恢複動作能繼續保持預期的保障。所以,作為一種策略,JVM不會自動中斷IO操作。

這給代碼強加了額外的職責來處理取消事件。若一個線程正在執行IO操作,如果在此IO操作期間試圖取消該IO操作,必須意識到IO對象正在使用且關閉該IO對象是你想要的行為。如果能接受這種情況,就可以通過關閉IO對象和中斷線程來完成活動取消。例如:


class CancellableReader {                        // Incomplete
	private Thread readerThread; // only one at a time supported
	private FileInputStream dataFile;

	public synchronized void startReaderThread() 
		throws IllegalStateException, FileNotFoundException {
		if (readerThread != null) throw new IllegalStateException();
			dataFile = new FileInputStream("data");
			readerThread = new Thread(new Runnable() {
				public void run() { doRead(); }
			});
		readerThread.start();
	}

	protected synchronized void closeFile() { // utility method
		if (dataFile != null) {
			try { dataFile.close(); } 
			catch (IOException ignore) {}
			dataFile = null;
		}
	}

	protected void doRead() {
		try {
			while (!Thread.interrupted()) {
				try {
					int c = dataFile.read();
					if (c == -1) break;
					else process(c);
				} catch (IOException ex) {
					break; // perhaps first do other cleanup
				}
			}
		} finally {
			closeFile();
			synchronized(this) { readerThread = null; }
		}
	}

	public synchronized void cancelReaderThread() {
		if (readerThread != null) readerThread.interrupt();
			closeFile();
	}
}

很多其它取消IO的場景源於需要中斷那些等待輸入而輸入卻不會或不能及時到來的線程。大部分基於套接字的流,可以通過設置套接字的超時參數來處理。其它 的,可以依賴InputStream.available,然後手寫自己的帶時間限製的輪詢循環來避免超時之後還阻塞在IO中(見§4.1.5)。這種設 計可以使用一種類似於§3.1.1.5中描述的有時間限製的退避重試協議。例如:


class ReaderWithTimeout {                // Generic code sketch
	// ...
	void attemptRead(InputStream stream, long timeout) throws... {
		long startTime = System.currentTimeMillis();
		try {
			for (;;) {
				if (stream.available() > 0) {
					int c = stream.read();
					if (c != -1) process(c);
					else break; // eof
				} else {
					try {
						Thread.sleep(100); // arbitrary fixed back-off time
					} catch (InterruptedException ie) {
						/* ... quietly wrap up and return ... */ 
					}
					long now = System.currentTimeMillis();
					if (now - startTime >= timeout) {
						/* ... fail ...*/
					}
				}
			}
		} catch (IOException ex) { /* ... fail ... */ }
	}
}

腳注:有些JDK發布版本也支持InterruptedIOException,但隻是部分實現了且僅限於某些平台。在本文撰寫之時,未來版本 打算停止對其支持,部分原因是由於IO對象不可用會引起不良後果。但既然InterruptedIOException定義為IOException的一 個子類,這種設計的工作方式與包含InterruptedIOException支持的版本上描述的相似,盡管存在額外的不確定性:中斷可能拋出 InterruptedIOException或InterruptedException。捕獲InterruptedIOException然後將其 作為一個InterruptedException重新拋出能部分解決該問題。

異步終止(Asynchronous termination)

stop方法起初包含在Thread類中,但是已經不推薦使用了。Thread.stop會導致不管線程正在做什麼就突然拋出一個ThreadDeath 異常。(與interrupt類似,stop不會中止鎖等待或IO等待。但與interrupt不同的是,它不嚴格保證會中止wait,sleep或 join)

這會是個非常危險的操作。因為Thread.stop產生異步信號,某些操作由於程序安全和對象一致性必須回滾或前滾,而活動正在執行這些操作或代碼段時可能被終止掉。看下麵例子:


class C {                                         // Fragments
	private int v;  // invariant: v >= 0

	synchronized void f() {
		v = -1  ;   // temporarily set to illegal value as flag
		compute();  // possible stop point (*)
		v = 1;      // set to legal value
	}

	synchronized void g() { 
		while (v != 0) { 
			--v; 
			something(); 
		} 
	}
}

如果Thread.stop碰巧導致(*)行終止,對象就被破壞了:線程一終止,對象將保持在不一致狀態,因為變量v被設了一個非法的值。其它線程在該對 象上的任何調用會執行不想要的或危險的操作。例如,這裏g方法中的循環將自旋2*Integer.MAX_VALUE次。stop讓回滾或前滾恢複技術的 使用變得極其困難。乍一看,這個問題看起來不太嚴重 —— 畢竟,調用compute拋出的任何未捕獲異常都會破壞狀態。但是,Thread.stop的後果更隱蔽,因為在可能忽略了ThreadDeath異常 (由Thread.stop拋出)而仍傳播取消請求的方法中你什麼也做不了。而且,除非在每行代碼後都放一個catch(ThreadDeath),否則 就沒辦法準確恢複當前對象的狀態,所以可能碰到未檢測到的破壞。相比之下,通常可以將代碼寫的健壯些,不用大費周章就能消除或處理其它類型的運行時異常。

換而言之,禁用Thread.stop不是為了修複它有缺陷的邏輯,而是糾正對其功能的錯誤認識。不可能允許所有方法的每條字節碼都能出現取消操作導致的異常(底層操作係統代碼開發者非常熟悉這個事實。即使程序非常短,很小的異步取消安全的例程也會是個艱巨的任務。)

注意,任意正在執行的方法可以捕獲並忽略由stop導致的ThreadDeath異常。這樣的話,stop就和interrupt一樣不能保證線程會被終止,這更危險。任何stop的使用都暗含著開發者評估過試圖突然終止某個活動帶來的潛在危害比不這樣做的潛在危害更大。

資源控製(Resource control)

活動取消可能出現在可裝載和執行外部代碼的任一係統的設計中。試圖取消未遵守標準約定的代碼麵臨著難題。外部代碼也許完全忽略了中斷,甚至是捕獲 ThreadDeath異常後將其丟棄,在這種情況下調用Thread.interrupt和Thread.stop將不會有什麼效果。

你無法精確控製外來代碼的行為及其耗時。但能夠且應當使用標準的安全措施來限製不良後果。一種方式是創建和使用一個SecurityManager,當某 個線程運行的時間太長,就拒絕所有對受檢資源的請求。(細節內容超出本書範圍,參考推薦讀物。)這種形式的資源拒絕同§3.1.2.2中討論的資源撤銷策 略一起能夠阻止外部代碼執行任一與其它應當繼續執行的線程競爭資源。副作用就是這些措施經常最終會導致線程因異常而掛掉。

此外,可以調用某個線程的setPriority(Thread.MIN_PRIORITY)將CPU資源的競爭降到最小。可以用一個SecurityManager來阻止該線程將優先級提高。

多步取消(Multiphase cancellation)

有時候,即使取消的是普通的代碼,損害也比通常的更大。為應付這種可能性,可以建立一個通用的多步取消功能,盡可能嚐試以破壞性最小的方式來取消任務,如果稍候還沒有終止,再使用一種破壞性較大的方式。

在大多數操作係統進程級,多步取消是一種常見的模式。例如,它用在Unix關閉期間,先嚐試使用kill -1終止任務,若有必要隨後再使用kill -9.大多數win係統中的任務管理器也使用了類似的策略。

這裏有個簡單版本的示例。(Thread.join使用方麵的更多細節參見§4.3.2.)


class Terminator {
	// Try to kill; return true if known to be dead

	static boolean terminate(Thread t, long maxWaitToDie) { 
		
		if (!t.isAlive()) return true;  // already dead
		// phase 1 -- graceful cancellation
		
		t.interrupt();       
		try { t.join(maxWaitToDie); } 
		catch(InterruptedException e){} //  ignore 

		if (!t.isAlive()) return true;  // success

		// phase 2 -- trap all security checks

		theSecurityMgr.denyAllChecksFor(t); // a made-up method
		try { t.join(maxWaitToDie); } 
		catch(InterruptedException ex) {} 

		if (!t.isAlive()) return true; 

		// phase 3 -- minimize damage

		t.setPriority(Thread.MIN_PRIORITY);
		return false;
	}
}

注意這裏的terminate方法本身忽略了中斷。這表明取消操作所做的這種策略選擇一旦開始就必須繼續。取消正在執行的取消操作,會給處理已經開始的與終止相關的清理帶來另外一些問題。

因不同JVM實現中Thread.isAlive的行為不盡相同(參見§1.1.2),當join因線程結束返回後,在線程完全死掉之前isAlive還有可能返回true。


文章轉自 並發編程網-ifeve.com

最後更新:2017-05-23 11:02:55

  上一篇:go  函數式編程 Functional Programming
  下一篇:go  Java IO: 其他字節流(上)