在Executor中一步一步提升併發

要把程序變爲併發程序,首先要理清各個任務之間的邊界。在大多數服務器應用程序中都存在一個明顯的任務邊界:即單個客戶請求。但有時候任務邊界也並不是是顯而易見的,好比在單個客戶請求中仍有可能存在可發掘的並行性,例如數據庫服務器。java

本文將開發一些不一樣版本的組件,而且每一個版本都實現了不一樣程度的併發性。該示例組件實現瀏覽器程序中的頁面渲染(Page-Rendering)功能,它的做用是將HTML頁面繪製到圖像緩存中。爲了簡便,假設HTML頁面只包含標籤文本,以及預約義大小的圖片和URL。數據庫

串行的頁面渲染

最簡單的方法就是對HTML文檔進行串行處理。當遇到文本標籤時,將其繪製到圖像緩存中。當遇到圖像引用時,先經過網絡獲取它,而後再將其繪製到圖像緩存中。這種方式很容易實現,程序只需將輸入中的每一個元素處理一次(甚至不須要緩存文檔),但這種方法會讓用戶等待很長時間,直到顯示全部的文本。瀏覽器

另外一種串行執行的方法會好一點,它先處理文本標籤,同時爲圖像預留出矩形的佔位空間,在處理完了第一遍文本後,程序在開始下載圖像,並將它們繪製到相應的佔位空間中:緩存

public class SingleThreadRenderer {
	void renderPage(CharSequence source) {
		//先處理文本
		renderText(source);
		
		//掃描整個文檔,找出全部的圖片,而後下載
		List<ImageInfo> imageInfos = scanForImageInfo(source);
		List<ImageData> imageDatas = new ArrayList<ImageData>();
		for (ImageInfo info : imageInfos) {
			imageDatas.add(info.downloadImage());
		}
		
		//渲染全部下載的圖片
		for (ImageData data : imageDatas) {
			renderImage(data);
		}
		
		//整個頁面渲染完成
	}
}

圖像下載過程的大部分時間都是在等待I/O操做執行完成,這期間CPU幾乎不做任何工做。所以,這種串行執行方法沒有充分的利用CPU,使得用戶在看到最終頁面以前要等待過長的時間。經過將問題分解爲多個獨立的任務併發執行,可以得到更高的CPU利用率和響應速度。服務器

使用Future實現

爲了實現更高的併發性,首先將渲染過程分解爲兩個任務,一個是渲染全部的文本標籤,另外一個是下載全部的圖像(由於其中一個任務時CPU密集型,而另外一個任務時I/O密集型,所以這種方法即便在單CPU系統上也能提高性能)。網絡

Callable和Future有助於表示這些協同任務之間的交互。下面的FutureRenderer中建立了一個Callable來下載全部的圖像,並將其提交到一個ExecutorService。這將返回一個描述任務執行狀況的Future。當主任務須要圖像時,它會等待Future.get的調用結果。若是幸運的話,當前開始請求時,全部的圖像就已經下載完成了,即便沒有,至少圖像的下載任務已經提早開始了:併發

public class FutureRenderer {
	private final ExecutorService executor = ...;
	void renderPage(CharSequence source) {
		//掃描整個文檔,找出全部的圖片
		final List<ImageInfo> imageInfos = scanForImageInfo(source);
		
		//定義一個任務,用於下載全部圖片
		Callable<List<ImageData>> task = new Callable<List<ImageData>>() {
			public List<ImageData>> call() {
				List<ImageData> result = new ArrayList<ImageData>();
				for (ImageInfo info : imageInfos) {
					ImageData data = info.downloadImage();
					result.add(data);
				}
				return result;
			}
		};
		
		//將任務提交到Executor中執行
		Future<List<ImageData>> future = executor.submit(task);
		
		//處理文本
		renderText(source);
		
		//獲取全部已下載好的圖片
		try {
			List<ImageData> imageDatas = future.get();//將阻塞直處處理完成或執行出錯
			//渲染全部下載的圖片
			for (ImageData data : imageDatas) {
				renderImage(data);
			}
		} catch(InterruptedException e) {
			Thread.currentThread().interrupt();
			future.cancel();
		} catch(ExecutionException e) {
			throw launderThrowable(e.getCause());
		}
		
		//整個頁面渲染完成
	}
}

這裏先介紹一下Future,Runnable和Callable函數

Runnable和Callable描述的都是抽象的計算任務。這些任務一般是有範圍的,即都有一個明確的起點,而且最終都會結束。性能

Future表示一個任務的生命週期,並提供了相應的方法來判斷是否已經完成或取消。Future還提供了get方法來得到計算結果。get方法的行爲取決於任務的執行狀態(還沒有開始,正在執行,已完成)。this

  • 若是任務已經完成(不論是正常結束仍是異常退出),get方法會當即返回或者拋出一個異常;
  • 若是沒有完成,get方法將阻塞直到任務完成(若是任務拋出了異常,將被封裝爲ExecutionException並從新拋出);
  • 若是任務被取消,get將拋出CancellationException。

FutureRenderer使得渲染文本任務與下載圖像的任務併發的執行。當全部圖像下載完後,會顯示到頁面上。這將提高用戶 體驗,不只使用戶更快的看到結果,還有效利用了並行性。但咱們還能夠作得更好——用戶沒必要等到全部的圖像都下載完成,而是但願看到每當下載完一幅圖像時就當即顯示出來。

使用CompletionService實現

CompletionService將Executor和BlockingQueue的功能融合到了一塊兒。你能夠將Callable任務提交給它來執行,而後使用相似於隊列操做的take和poll等方法來得到已完成的結果,而這些結果會在完成時被封裝爲Future。

咱們能夠經過CompletionService從兩個方面來提升頁面渲染的性能:縮短總運行時間以及提升響應性。爲每一幅圖像的下載都建立一個獨立任務,並在線程池中執行它們,從而將下載過程轉換爲並行過程,這將減小下載全部圖像的總時間。此外,經過從CompletionService中獲取結果以及使每張圖片在下載完成好偶馬上顯示出來,能使用戶得到一個更加動態和更高響應性的用戶界面:

public class Renderer {
	private final ExecutorService executor;
	
	//構造函數
	public Renderer(ExecutorService executor) {
		this.executor = executor;
	}
	
	void renderPage(CharSequence source) {
		//掃描整個文檔,找出全部的圖片
		final List<ImageInfo> imageInfos = scanForImageInfo(source);
		
		//使用CompletionService併發處理下載圖片的任務
		CompletionService<ImageData> completionService = 
				new ExecutorCompletionService<ImageData>(executor);
		for (final ImageInfo info : imageInfos) {
			completionService.submit(new Callable<ImageData>(){
				public ImageData call() {
					return info.downloadImage();
				}
			});
		}
		
		//處理文本
		renderText(source);
		
		//獲取全部已下載好的圖片
		try {
			//渲染全部下載的圖片
			for (int i = 0; i < imageInfos.size(); i++) {
				Future<ImageData> future = completionService.take();
				ImageData img = future.get();//將阻塞直處處理完成或執行出錯
				renderImage(img);
			}
		} catch(InterruptedException e) {
			Thread.currentThread().interrupt();
		} catch(ExecutionException e) {
			throw launderThrowable(e.getCause());
		}
		
		//整個頁面渲染完成
	}
}

多個ExecutorCompletionService能夠共享一個Executor,所以能夠建立一個對於特定計算私有,又能共享一個公共Executor的ExecutorCompletionService。所以CompletionService的做用就至關於一組計算的句柄,這與Future做爲單個計算的句柄是很是相似的。

相關文章
相關標籤/搜索