页面渲染器
本文将使用不同方式实现 Web 页面渲染器,并分析不同实现方式的优缺点。
SingleThreadRenderer
SingleThreadRenderer 是一个串行页面渲染器,它先渲染绘制文本元素,同时为图像预留出矩形空间,待文本渲染完毕后开始下载图像,图像下载完后将它们渲染并绘制在相应的预留空间中。
由于图像下载的过程中大部分时间在等待 I/O,而 CPU 是空闲的,浪费了计算资源。故为了获得更高的 CPU 利用率和响应性,可以将问题拆分成多个独立的任务并发执行。
public class SingleThreadRenderer {
public void renderPage(CharSequence source) {
// 渲染文字
renderText(source);
List<ImageData> imageData = new ArrayList<>();
// 从HTML中扫描图片url,并根据 url 下载图片数据
for (ImageInfo imageInfo : scanForImageInfo(source)) {
imageData.add(imageInfo.downloadImage());
}
// 渲染图片
for (ImageData data: imageData) {
renderImage(data);
}
}
private void renderText(CharSequence source) {
System.out.println("render text...");
};
private void renderImage(ImageData imageData) {
System.out.println("render image...");;
}
private ImageInfo[] scanForImageInfo(CharSequence source) {
System.out.println("scan for image info...");
return null;
}
class ImageInfo {
public ImageData downloadImage() {
System.out.println("downloading image...");
return null;
}
}
class ImageData {
}
}
FutureRenderer
FutureRenderer 中创建了一个 Callable 来下载所有图像,并将其提交到一个 ExecutorService, 提交后会返回一个描述任务执行情况的 Future。当主任务需要图象时,可以调用 Future.get 获取,它是一个阻塞方法,如果图像已经下载完毕可以立即获取,否则等待下载任务完成后再获取。
get 方法的异常处理代码将处理两个问题:
- 任务执行时遇到 Exception;
- 调用 get 的线程在获得结果之前被中断。
FutureRenderer 虽然可以使文本渲染任务和图像下载任务并发执行,但仍有不足之处:用户不必等待所有图像下载完成,而是希望每当下载完一幅图像时便立即显示。
FutureRenderer 其实是属于异构任务并行化,而异构任务并行化是有其局限性的,体现在如果渲染文本的速度远远高于下载图像的速度(可能性很大),那么程序的性能相较于串行化提升很小,而代码却更加复杂。要知道,当使用两个线程时,相较于只使用一个线程的情况,至多能将速度提高一倍。但对于异构任务,很多时候提升十分有限。
public class FutureRenderer {
private final ExecutorService executor = Executors.newFixedThreadPool(10);
public void renderPage(CharSequence source) {
final List<ImageInfo> imageInfos = scanForImageInfo(source);
// task 任务负责下载所有图片,并将图片数据保存在 list 中返回
Callable<List<ImageData>> task = new Callable<List<ImageData>>() {
@Override
public List<ImageData> call() throws Exception {
List<ImageData> rst = new ArrayList<ImageData>();
for (ImageInfo imageInfo : imageInfos) {
rst.add(imageInfo.downloadImage());
}
return rst;
}
};
Future<List<ImageData>> future = executor.submit(task);
renderText(source);
try {
List<ImageData> imageData = future.get();
for (ImageData data : imageData)
renderImage(data);
} catch (InterruptedException e) {
// 重新设置线程的中断状态
Thread.currentThread().interrupt();
// 由于不需要结果,因此取消任务
future.cancel(true);
} catch (ExecutionException e) {
}
}
private void renderText(CharSequence source) {
System.out.println("render text...");
};
private void renderImage(ImageData imageData) {
System.out.println("render image...");;
}
private List<ImageInfo> scanForImageInfo(CharSequence source) {
System.out.println("scan for image info...");
return null;
}
class ImageInfo {
public ImageData downloadImage() {
System.out.println("downloading image...");
return null;
}
}
class ImageData {
}
}
CompletionServiceRenderer
CompletionService 相比于 FutureRenderer 从两个方面提高了页面渲染器的性能:
- 缩短总运行时间以提高响应性:为每一幅图像的下载都创建一个独立的任务,并在线程池中执行他们,从而将串行的下载过程转换为并行的过程(每幅图的下载任务是同构任务,可以获得较好的并行性能)。
- 使用户获得一个更加动态和更高响应性的用户界面:通过从 CompletionService 中获取结果以及使每张图片在下载完成后立刻显示出来。
public class CompletionServiceRenderer {
private final ExecutorService executor;
CompletionServiceRenderer(ExecutorService executor) {
this.executor = executor;
}
public void renderPage(CharSequence source) {
// 从源中检索所有的图片url
List<ImageInfo> info = scanForImageInfo(source);
CompletionService<ImageData> completionService = new ExecutorCompletionService<>(executor);
// 为每个图片url建立一个 Callable 下载任务,并提交给 completionService
for (final ImageInfo imageInfo : info) {
completionService.submit(new Callable<ImageData>() {
@Override
public ImageData call() throws Exception {
return imageInfo.downloadImage();
}
});
}
// 渲染文字
renderText(source);
// 阻塞地获取已下载完成的图片数据
for (int t = 0, n = info.size(); t < n; t++) {
try {
Future<ImageData> f = completionService.take();
ImageData imageData = f.get();
// 每获取一张图片数据就进行渲染
renderImage(imageData);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
// 渲染文本
private void renderText(CharSequence source) {
System.out.println("render text...");
};
// 渲染一张图片
private void renderImage(ImageData imageData) {
System.out.println("render image...");;
}
// 从源中检索图片 url
private List<ImageInfo> scanForImageInfo(CharSequence source) {
System.out.println("scan for image info...");
return null;
}
class ImageInfo {
// 根据图片 url 下载图片
public ImageData downloadImage() {
System.out.println("downloading image...");
return null;
}
}
// 图片数据类
class ImageData {
}
}
RenderPageWithAd
RenderPageWithAd 中,加载的页面包括响应用户请求的内容以及从广告服务器上获取的广告。它将获取广告的任务提交给 Executor,然后计算剩余的文本页面内容,最后等待广告信息。为了提升用户体验,在使用 Future 的 get 方法获取广告信息时,设置了时限,如果超过时限还未能成功获取到广告,则 get 方法会抛出 TimeoutException,在处理该异常的代码中利用 Future 将该任务取消,转而加载默认的广告。
public class RenderPageWithAd {
private final long TIME_BUDGET = 1000;
private final Ad DEFAULT_AD = new Ad();
private ExecutorService executor = Executors.newFixedThreadPool(10);
Page renderPageWithAd() throws InterruptedException {
long endNanos = System.nanoTime() + TIME_BUDGET;
Future<Ad> f = executor.submit(new FetchAdTask());
// 在等待广告的同时显示页面
Page page = renderPageBody();
Ad ad;
try {
// 只等待指定的时间长度
long timeLeft = endNanos - System.nanoTime();
ad = f.get(timeLeft, TimeUnit.NANOSECONDS);
} catch (ExecutionException e) {
ad = DEFAULT_AD;
} catch (TimeoutException e) {
ad = DEFAULT_AD;
f.cancel(true);
}
setAd(ad);
return page;
}
private void setAd(Ad ad) {}
private Page renderPageBody() {
return new Page();
}
class Page {
}
class Ad {
}
class FetchAdTask implements Callable<Ad> {
@Override
public Ad call() throws Exception {
return null;
}
}
}
TravelQuoteRenderer
TravelQuoteRenderer 可以在指定时间内请求旅游报价:用户输入旅行的日期和其它要求,门户网站获取并显示来自多条航线、旅店或汽车租赁公司的报价。在获取不同公司报价的过程中,可能会调用 Web 服务、访问数据库、执行一个 EDI 事务或其它机制。这种情况下,不应该让页面的响应时间受限于最慢的响应时间,而因该只显示在指定时间内收到的信息,对于没有及时响应的服务提供者,页面可以忽略它们。
TravelQuoteRenderer 的一种实现方式是将获取报价的过程当成一个任务,创建 n 个这样的任务并提交到线程池,保留返回的 n 个 Future,并使用限时的 get 方法通过 Future 串行地获取每一个结果。
本示例使用了一种更简单的方法:invokeAll。该方法可以将多个任务提交到 ExecutorService 并获取结果。invokeAll 方法的参数是一组任务,并返回一组 Future。它按照任务集合中迭代器的顺序将所有的 Future 添加到返回的集合中,使得调用者可以将各个 Future 与其代表的 Callable 关联起来。当所有任务执行完毕时,或者调用线程被中断,又或者超过指定时限时,invokeAll 将返回。当超过指定时限后,任何为完成的任务都会取消。当 invokeAll 返回后,每个任务要么正常地完成,要么被取消,可以调用 Future 的 get 或 isCancelled 来判断任务的情况。
public class TravelQuoteRenderer {
private ExecutorService exec = Executors.newFixedThreadPool(10);
public List<TravelQuote> getRankedTravelQuotes(TravelInfo travelInfo
, Set<TravelCompany> companies, Comparator<TravelQuote> ranking,
long time,
TimeUnit unit) throws InterruptedException {
List<QuoteTask> tasks = new ArrayList<>();
// 为每个公司创建一个向其询问报价的任务
for (TravelCompany company : companies) {
tasks.add(new QuoteTask(company, travelInfo));
}
// 调用 invokeAll 方法批量提交一组任务,且为这组任务设置时限
List<Future<TravelQuote>> futures = exec.invokeAll(tasks, time, unit);
List<TravelQuote> quotes = new ArrayList<TravelQuote>(tasks.size());
Iterator<QuoteTask> taskIterator = tasks.iterator();
// 按迭代器顺序获取任务结果
for (Future<TravelQuote> f : futures) {
QuoteTask task = taskIterator.next();
try {
quotes.add(f.get());
} catch (ExecutionException e) {
quotes.add(null);
} catch (CancellationException e) {
quotes.add(null);
}
}
Collections.sort(quotes, ranking);
return quotes;
}
}
class QuoteTask implements Callable<TravelQuote> {
private final TravelCompany company;
private final TravelInfo travelInfo;
public QuoteTask(TravelCompany company, TravelInfo travelInfo) {
this.company = company;
this.travelInfo = travelInfo;
}
@Override
public TravelQuote call() throws Exception {
return null;
}
}
class TravelCompany {
}
class TravelInfo {
}
class TravelQuote {
}