在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做为单个计算的句柄是很是相似的。

相关文章
相关标签/搜索