为 Intel AppUp(SM) 中心开发 HTML5 应用程序 -第二部分:Web Workers





5.00/5 (3投票s)
这是关于为 Intel AppUp (SM) 中心开发 HTML5 应用程序的系列文章的第二篇。本文将介绍 HTML5 的一项新功能——Web Workers。我们将展示一个说明 Web Workers 使用情况的应用程序,以及其他一些 HTML5 功能。
目录
引言
在本系列关于为 AppUp 开发 HTML5 应用程序的第二篇文章中,我们将介绍 HTML5 的一项新功能——Web Workers。我们将展示一个说明 Web Workers 使用情况的应用程序,以及其他一些 HTML5 功能,如 Canvas。最后,我们将讨论 Web Workers 的其他可能应用。
本文中描述的所有源文件都可以从 此处 下载。
Web Workers
通常,在浏览器中运行的 JavaScript 代码在单个线程中执行。如果您需要执行一些 CPU 密集型计算,这可能会导致 JavaScript 代码运行缓慢并影响用户响应能力。能够在另一个线程或后台进程中运行一些 JavaScript 代码将解决这个问题。此外,现代多核 CPU 可以并行执行多个线程,因此我们希望支持多线程以利用硬件的功能。
Web Workers 是支持多线程执行的新 HTML5 功能。Web Worker 是一个线程,它允许您在与主浏览器 UI 线程并行执行的任务在后台进程中运行。Web Worker 是一个 JavaScript 文件,其中包含您要在单独线程中执行的任务。Web Workers 允许您处理计算密集型任务,而不会阻塞 UI。
Web Workers 的一个常见应用是在不中断用户界面的情况下执行计算密集型任务。它们还可以用于耗时操作,如网络操作。
多线程的本质会引发共享数据可能存在的同步问题,这些数据会被多个线程访问。因此,Web Workers 施加了一些限制,最值得注意的是限制 Web Workers 访问 DOM、window 对象、document 对象和 parent 对象。
本文中的示例应用程序演示了使用 Web Workers 执行 CPU 密集型操作,特别是计算素数。
问题
回想一下,素数是指大于 1 且除了 1 和自身以外没有正因数的数。素数在计算的许多领域都很重要,例如加密,但对我们而言,它是一个很好的 CPU 密集型计算示例,可以说明 Web Workers 的使用。
一种确定一个数是否为素数的简单方法称为试除法。它包括测试给定数 n 是否是 2 到 n 的平方根之间的任何整数的倍数。如果将 n 除以试除整数后余数为零,则 n 不是素数。存在比试除法更有效的算法,但对我们而言,它是一个很好的 CPU 密集型问题,可以应用 Web Workers。
我们的示例应用程序将计算素数并显示最近找到的素数。我们还将显示自计算开始以来的经过时间。由于素数有无穷多个,程序将永远不会完成。我们将提供一个“停止”按钮来停止计算,一个“关闭”按钮来退出应用程序。
为了表明计算确实发生在单独的线程中,我们将使用 HTML5 Canvas 功能显示一个时钟动画。
代码说明
首先,我们需要一个名为 icon.png 的标准图标文件,以满足 Intel AppUp™ 封装器的要求。
我们使用样式表来获得我们想要的按钮外观、结果文本字体等。您可以随时研究此 app.css 文件。
执行从 index.html 文件开始。如列表 1 所示,我们包含了两个 JavaScript 文件:stopwatch.js 和 main.js。
<!DOCTYPE HTML>
<html>
<head>
<title>Web Workers</title>
<link href="app.css" rel="stylesheet" type="text/css" />
<script src="stopwatch.js" type="text/javascript"></script>
<script src="main.js" type="text/javascript"></script>
</head>
然后,我们设置背景图像并调用 JavaScript 函数来初始绘制秒表,然后声明一些我们将要使用的变量。
<body background="numbers.jpg" onload="stopwatch();">
<script type="text/javascript">
var running = false;
var worker;
var timerId = 0;
var seconds = 0;
var date;
</script>
文件的其余部分显示 UI 字符串,定义我们将用于绘制动画秒表的 canvas,并定义“开始”、“停止”和“关闭”按钮,将它们与单击时运行的 JavaScript 函数关联起来。请注意,经过时间和最高素数结果使用 <output>
标签。稍后我们将看到它们如何与生成结果的代码连接起来。
<h1>Web Workers Example: Prime Numbers</h1>
<hr>
<h3>Press the Start button to start the calculation process.<br>
Press the Stop button to stop calculating.</h3>
<canvas id="stopwatch" width=150 height=150>Your browser doesn't support HTML5 canvas</canvas>
<h3>Elapsed time: <output id="elapsedTime"></output><br>
Highest prime number found: <output id="primeNumber"></output></h3>
<hr>
<br>
<a href="#" id="Start" class="button white" onclick="startWorker()">Start</a>
<a href="#" id="Stop" class="button white" onclick="stopWorker()">Stop</a>
<a href="#" id="Close" class="button white" onclick="closeApplication()">Close</a>
</body>
</html>
现在让我们看看 main.js 的代码,如下面的列表 4 所示。当单击“开始”按钮时,将调用 startWorker 函数。如果 worker 线程尚未运行,它会将 running 变量设置为 true,elapsed time 设置为零,并保存当前日期和时间。它调用 stopwatch.js 中的 init()
,我们稍后将看到。
然后,我们创建一个新的 Worker,即 JavaScript Web Worker 类型。我们为此添加了一个事件监听器,并将消息处理程序设置为我们将稍后看到的函数 e。我们将事件发送到我们在 main.html 中定义的 output 标签,使用其标识符 primeNumber
。我们获取当前时间并将其写入我们在 main.html 中定义的 output 标签字段,使用其标识符 elapsedTime
。最后,我们向 worker 发送一条消息以启动它。
function startWorker()
{
if (!running)
{
running = true;
seconds = 0;
date = new Date;
init();
worker = new Worker("worker.js");
worker.addEventListener('message', onmessage, false);
worker.onmessage = function(e)
{
document.getElementById('primeNumber').textContent = event.data;
date.setHours(0);
date.setMinutes(0);
date.setSeconds(seconds - 1);
document.getElementById('elapsedTime').textContent = date.toTimeString("hh:mm:ss").substring(0, 8);
}
worker.postMessage(); // Start worker
}
}
function stopWorker()
{
if (running)
{
running = false;
clearInterval(timerId);
worker.terminate(); // Stop worker
}
}
function closeApplication()
{
intel.adp.encapsulator.closeapplication()
}
stopWorker 函数在单击“停止”按钮时调用。它将 running 变量设置为 false,并停止一个计时器(我们在调用 init()
时创建的,稍后我们将看到)。然后它终止 Web Worker。这很重要,因为 Web Worker 使用的某些资源仅在终止时才会被释放。closeApplication
函数由“关闭”按钮调用,它只是关闭应用程序。
现在让我们看看 worker.js 文件,如列表 5 所示。此文件中的消息处理程序函数在后台由与主 GUI JavaScript 线程不同的线程运行。它使用试除法从 2 开始搜索素数。找到素数后,它会发送一条消息,将该数字作为参数传递。该数字将被写入我们在主网页中设置的 output 字段。
addEventListener('message', onmessage, false);
onmessage = function(e)
{
var n = 1;
search:
while (true)
{
n += 1;
for (var i = 2; i <= Math.sqrt(n); i += 1)
if (n % i == 0)
continue search; // Number is not prime.
// Number is prime, report it.
postMessage(n);
}
}
最后一个文件是 stopwatch.js,如列表 6 所示。文件中的第一个函数是 init。它从 startWorker
调用。它首先调用 stopwatch 函数,然后创建一个每秒调用 stopwatch 函数一次的计时器。
文件中的另一个函数 stopwatch 使用 Canvas 元素绘制一个带有秒针的动画时钟。秒针的位置是使用基于当前时间计算出的秒数的值绘制的。代码篇幅较长,但很简单。您可以随时研究它。
function init()
{
stopwatch();
timerId = setInterval(stopwatch, 1000);
}
function stopwatch()
{
var elem = document.getElementById('stopwatch');
if (elem && elem.getContext)
{
var context = elem.getContext('2d');
if (context)
{
context.save();
context.clearRect(0, 0, 150, 150);
context.translate(75, 75);
context.scale(0.5, 0.5);
context.rotate(-Math.PI / 2);
context.strokeStyle = "grey";
context.fillStyle = "white";
context.lineWidth = 6;
context.lineCap = "square";
context.save();
context.lineWidth = 5;
for (i = 0; i < 60; i++)
{
if (i % 5 != 0)
{
context.beginPath();
context.moveTo(120, 0);
context.lineTo(115, 0);
context.stroke();
}
context.rotate(Math.PI / 30);
}
context.restore();
context.save();
for (var i = 0; i < 12; i++)
{
context.beginPath();
context.rotate(Math.PI / 6);
context.moveTo(120, 0);
context.lineTo(100, 0);
context.stroke();
}
context.restore();
context.fillStyle = "black";
context.save();
context.rotate(seconds++ * Math.PI / 30);
context.lineWidth = 6;
context.lineCap = "round";
context.strokeStyle = "red";
context.beginPath();
context.moveTo(-30, 0);
context.lineTo(100, 0);
context.stroke();
context.beginPath();
context.arc(0, 0, 10, 0, 2 * Math.PI, false);
context.fill();
context.restore();
context.beginPath();
context.strokeStyle = "#417FA1";
context.lineWidth = 10;
context.arc(0, 0, 140, 0, 2*Math.PI, false);
context.stroke();
context.restore();
}
}
}
图 1 显示了正在运行的应用程序的屏幕截图。
由于素数有无穷多个,该算法将永远不会完成。我们的示例程序最终会失败,因为其使用的数字值会变得非常大,以至于 JavaScript 的浮点数表示会失去精度,以至于将该值加一仍返回相同的值。这种情况在执行数小时后才会发生。
其他应用
对于 CPU 密集型应用程序,您可能会认为使用 JavaScript 这样的解释型语言会很慢,但现代 JavaScript 引擎的性能可能会出奇地好。作为一项非常简单的基准测试,我测量了 Web Workers 应用程序计算 1000 万以内的素数所花费的时间。我将其与使用相同算法计算素数的本地 C++ 程序进行了比较。HTML5 应用程序花费了大约 2 分钟来计算 1 到 1000 万之间的素数。C++ 程序在相同的硬件上花费了大约 35 秒。这是一个有些不公平的比较,因为 HTML5 程序还需要以漂亮的格式输出结果。即便如此,JavaScript 代码的性能仍然与原生代码在同一数量级。
如果您需要优化原生代码的性能,一种选择是混合应用程序,它同时具有浏览器(HTML5)代码和原生代码。当然,这会牺牲可移植性。
Web Workers 可能有用的其他领域包括:
- 在后台执行网络输入/输出
- 富文本(例如源代码)语法高亮
- 对从
<canvas>
或<video>
元素提取的数据进行图像处理 - 更新客户端数据库
结论
在本文中,我们介绍了 HTML Web Workers 功能。我们展示了一个使用 Web Workers 的简单示例应用程序,并讨论了一些更现实的实际应用场景。