第26章 多线程处理
第26章 多线程处理
在运行大型、复杂的JavaScript脚本时经常会发生浏览器假死现象,那么能不能让这些代码在后台运行,或者让JavaScript函数在多个进程中同时运行。HTML5 Web Workers正是为了要解决这些问题而出现的。
HTML5 Web Workers可以让Web应用程序具备后台处理能力。它支持多线程处理功能,因此使用了HTML5的JavaScript应用程序可以充分利用多核CPU带来的优势。将耗时长的任务分配给HTML5 Web Worker执行,这样就避免了页面有时反应迟缓,甚至假死现象。Web Workers可以创建一个不影响前台处理的后台线程,并且在这个后台线程中创建多个子线程。这样可以将耗时较长的处理交给后台线程去运行,从而解决了HTML5之前因为某个处理耗时过长而跳出一个提示用户脚本运行时间过长,导致用户不得不结束这个处理的尴尬状况。
本章将介绍Web Workers的基本知识,帮助读者使用Web Workers在Web网站或应用程序中创建一个后台线程。掌握在前台页面与后台线程进行数据交互时所使用到的方法与事件,能够在JavaScript脚本中实现前台页面与后台线程之间的数据交互,能够在主线程之间嵌套子线程的方法,利用JavaScript脚本在主线程中创建一个或多个子线程,能够实现主线程与子线程、子线程与子线程之间的数据传递。了解在后台线程中可以使用的JavaScript脚本中的对象、方法和事件。
【学习重点】
▲ 了解多线程技术
▲ 能够使用HTML5 Web Workers
▲ 利用多线程技术解决JavaScript计算能力
26.1 Web Workers概述
Web Workers为网页脚本提供了一种能在后台进程中运行的方法。当创建Worker对象后,Web Workers就可以通过postMessage()方法向任务池发送任务请求,执行完之后再通过postMessage返回消息给创建者指定的事件处理程序,然后通过onmessage捕获返回消息,实现前后台数据的交互。
26.1.1 认识Web Workers
在Web应用程序中,Web Workers是一项后台处理技术。在此之前使用JavaScript创建的Web程序中,因为所有的处理都是在单线程内执行,所以如果脚本需要很长时间运行,程序界面会长时间处于停止响应状态。甚至当等待时间超出一定的限度,浏览器提示脚本运行时间过长需要中断正在执行的处理。
为了解决这个问题,HTML5新增了一个Web Workers API。使用这个API,用户可以很容易地创建在后台运行的线程,这个线程被称为worker,如果将可能耗费较长时间的处理交给后台去执行,对用户在前台页面中执行的操作就没有影响。
尽管Web Workers功能强大,但也不是万能的,有些事情它还做不到。例如,在Web Workers中执行的脚本不能访问该页面的window对象,因此Web Workers不能直接访问Web页面和DOM API,虽然Web Workers不会导致浏览器UI停止响应,但是仍然会消耗CPU周期,导致系统反应速度变慢。
如果开发人员创建的Web应用程序需要执行一些后台数据处理,但又不希望这些数据处理任务影响Web页面本身的交互性,那么可以通过Web Workers生成一个Web Worker去执行数据处理任务。同时添加一个事件监听器进行监听,并与之进行数据交互。
Web Workers的另一个用途是可以监听由后台服务器广播的消息,收到后台服务器的消息后,将其显示在Web页面上。这种与后台服务器对话的场景,Web Workers可能会使用到Web Sockets或Server-Sent事件。
Web Workers接口可以创建真正的系统级别的进程,它还可以使用XMLHttpRequest来处理I/O,无论responseXML和channel属性是否为null。使用它可以很容易设计并发操作效果。例如,在做网站下载时使用Worker,或者使用Worker实现处理扩展功能。
注意:后台进程(包括Web Workers进程)不能对DOM进行操作。如果希望后台程序处理的结果能够改变DOM,只能通过返回消息给创建者的回调函数进行处理。
Web Workers能够为我们做些什么?
☑ 加载一个JavaScript文件,进行大量的复杂计算,而不挂起主进程,并通过postMessage和onmessage进行通信。
☑ 可以在worker中通过importScripts(url)方法加载JavaScript脚本文件。
☑ 可以使用setTimeout()、clearTimeout()、setInterval()和clearInterval()。
☑ 可以使用XMLHttpRequest进行异步请求。
☑ 可以访问navigator的部分属性。
☑ 可以使用JavaScript核心对象。
Web Workers的局限性:
☑ 不能跨域加载JavaScript。
☑ Worker内代码不能访问DOM。
☑ 各个浏览器对Worker的实现还没有完全完善。不是每个浏览器都支持所有新特性。
☑ 使用Web Workers加载数据没有JSONP和Ajax加载数据高效。
26.1.2 浏览器支持概述
各浏览器对HTML5 Web Workers的支持情况如表26-1所示,从中可以看到目前浏览器对Web Workers的支持情况各不相同,但Web Workers已经得到了大部分主流浏览器的支持,并且仍在持续更新发展中。
表26-1 浏览器支持概述
26.1.3 熟悉Web Workers成员
Web Workers的使用方法非常简单,只需创建一个Web Workers对象,并传入希望执行的JavaScript文件即可。然后在页面中设置一个事件监听器,用来监听由Web Workers对象发来的消息和错误信息。如果想要在页面与Web Workers之间建立通信,数据需通过postMessage()函数传递。对于Web Workers JavaScript文件中的代码也是如此,必须通过设置事件处理程序来处理发来的消息和错误信息,通过调用postMessage()函数实现与页面的数据交互。
在使用Web Workers之前,读者应该熟悉线程中可用的变量、函数与类。在线程调用的JavaScript脚本文件中所有可用的变量、函数与类如下。
☑ self:self关键字用来表示本线程范围内的作用域。
☑ postMessage(meseage):向创建线程的源窗口发送消息。
☑ onmessage:获取接收消息的事件句柄。
☑ importScripts(urls):导入其他JavaScript脚本文件。参数为该脚本文件的URL地址,可以导入多个脚本文件。导入的脚本文件必须与使用该线程文件的页面在同一个域中,并在同一个端口中。
importScripts(“worker.js”,“worker1.js”,“worker2.js”);
☑ navigator对象:与window.navigator对象类似,具有appName、platform、userAgent、appVersion属性。它们可以用来标识浏览器的字符。
☑ sessionStorage/localStorage:在线程中可以使用Web Storage。
☑ XMLHttpRequest:在线程中可以处理Ajax请求。
☑ Web Workers:在线程中可以嵌套线程。
☑ setTimeout()/setInterval():在线程中可以实现定时处理。
☑ close:结束本线程。
☑ eval()、isNaN()、escape()等:可以使用所有JavaScript核心函数。
☑ object:可以创建和使用本地对象。
☑ WebSockets:可以使用Web Sockets API向服务器发送和接收信息。
26.2 使用WebWorkers
使用Web Workers的步骤十分简单。只要在Worker类的构造器中,将需要在后台线程中执行的脚本文件的URL地址作为参数,然后创建Worker对象。
var worker=new Worker(“worker.js”);
注意:在后台线程中不能访问页面或窗口对象,此时如果在后台线程的脚本文件中使用window对象或document对象,则会引发错误。
另外,可以通过发送和接收消息与后台线程互相传递数据。通过对Worker对象的onmessage事件获取可以在后台线程中接收消息。
使用Worker对象的postMessage()方法可以给后台线程发送消息。发送的消息是文本数据,但也可以是任何JavaScript对象,需要通过JSON对象的stringify()方法将其转换成文本数据。
worker.postMessage(meseage);
通过获取Worker对象的onmessage事件句柄及Worker对象的postMessage()方法可以实现线程内部的消息接收和发送。
26.2.1 检查浏览器支持性
在调用Web Workers API函数之前,应该确认当前浏览器是否支持。如果不支持,可以提供一些备用信息,提醒用户使用最新的浏览器。
【示例】下面代码可以用来测试浏览器是否支持。
在上面代码中,使用testWorker()函数来检测浏览器的支持情况,可在页面加载时调用该函数。调用typeof(Worker)会返回全局Window对象的Worker属性,如果浏览器不支持Web Workers API,则返回结果将是undefined。上面这段代码在检测了浏览器支持性之后,会将检测结果反馈到页面上。
26.2.2 创建Web Workers
调用Worker构造函数就可以简单创建一个worker。Web Workers在初始化时会接收一个JavaScript文件的URL地址,参数URI表示要执行的脚本文件地址。其中包含了供Worker执行的代码。这段代码会设置事件监听器,并与生成Workers的容器进行通信。JavaScript文件的URL可以是相对或者绝对路径,只要是相同的协议、主机和端口即可。
worker=new Worker(“echoWorker.js”);
如果想获取worker进程的返回值,可以通过它的onmessage属性来绑定一个事件处理程序:
第一行用来创建和运行worker进程,第二行设置worker的onmessage属性用来绑定指定的事件处理程序,当worker的postMessage()方法被调用时,这个被绑定的程序就会被触发。
对于由多个JavaScript文件组成的应用程序来说,可以通过包含script元素的方式,在页面加载时同步加载JavaScript文件。由于Web Workers没有访问document对象的权限,所以Worker只能使用importScripts()方法导入其他JavaScript文件。importScripts()是全局函数,该函数可以将脚本或库导入到它们的作用域中,导入的JavaScript文件只会在某一个已有的Worker中加载和执行。多个脚本的导入同样也可以使用importScripts()函数,它们会按顺序执行。
importScripts()可以接收空的参数或多个脚本URI参数,下面这些形式都是合法的:
importScripts();
importScripts(‘foo.js’);
importScripts(‘foo.js’,‘bar.js’);
JavaScript会加载列出的每一个脚本文件,然后运行并初始化。这些脚本中的任何全局对象都可以被worker使用。
提示:importScripts下载脚本顺序可能不一样,但执行的顺序一定是按importScripts中列出的顺序进行,而且是同步的,在所有脚本加载完并运行结束后importScripts才会返回。
Web Workers能够嵌套使用,以创建子Worker:
var subWorker=new Worker(“subWorker.js”);
用户可以创建多个workers。子worker必须寄宿于同一个父页面下,且它的URI必须与父worker的地址同源,这样可以很好地维持它们的依赖关系。
Web Workers可以使用setTimeout和setInterval。如果希望Web Workers进程周期性地运行而不是不停地循环下去,使用这两个方法非常有用。
26.2.3 与Web Workers通信
使用后台线程时不能访问页面或窗口对象,但是并不代表后台线程不能与页面之间进行数据交互。为了实现页面与Web Workers通信,可以调用postMessage()函数传入所需数据。同时将建立一个监听器,用来监听由Web Workers发送到页面的消息。
为建立页面和Web Workers之间的通信,首先在页面中添加对postMessage()函数的调用,如下所示:
当用户单击按钮后,相应信息会被发送给Web Workers,然后将事件监听器添加到页面中,用来监听从Web Workers发来的信息。
编写HTML5 Web Workers JavaScript文件。在该文件中,需要添加事件监听器以监听发来的消息,并且通过调用postMessage()函数实现与页面之间的通信。
为了完成页面与Web Worker之间的通信功能。首先,添加代码调用postMessage()函数。例如,在messageHandler()函数中可以添加如下代码:
接下来,在Web Workers JavaScript文件中添加事件监听器,以处理从页面发来的信息:
addEventListener(“message”, messageHandler, true);
接收到信息后会马上调用messageHandler函数以保证信息能及时返回。
通过postMessage()函数将对象传递到workers或者从中返回对象,这些对象将被自动转换为JSON格式。
提示:在workers中进出的对象不能包含函数和循环引用,因为JSON不支持它们。
在Web Workers脚本中如果发生未处理的错误,会引发Web Workers对象的错误事件。特别是在调试用到Web Workers脚本时,对错误事件的监听就显得尤为重要。下面显示的是Web Workers JavaScript文件中的错误处理函数,它将错误记录在控制台上。
为了处理错误,还必须在主页上添加一个事件监听器:
worker.addEventListener(“error”, errorHandler, true);
当worker发生运行时错误时,它的onerror事件就会被触发。该事件接收一个error的事件,该事件不会冒泡,并且可以取消。要取消该事件可以使用preventDefault()方法。该错误事件有3个属性。
☑ message:可读的错误信息。
☑ filename:发生错误的脚本文件名称。
☑ lineno:发生错误的脚本所在文件的行数。
Web Workers不能自行终止,但能够被启用它们的页面所终止。调用terminate()函数可以终止后台进程。被终止的Web Workers将不再响应任何信息或者执行任何其他的计算。终止之后,Worker不能被重新启动,但可以使用同样的URL创建一个新的Worker。
worker.terminate();
如果需要马上终止一个正在运行中的worker,可以调用它的terminate()方法:
myWorker.terminate();
这样一个worker进程就被结束了。
26.2.4 案例:使用Web Workers
【示例1】本示例演示了如何使用Web Workers在控制台显示一个提示信息。首先,设计主页面代码如下:
线程脚本文件worker.js的代码如下:
在Chrome浏览器中访问主页文件,则可以在控制台中看到输出的信息,表示程序执行成功,如图26-1所示。
图26-1 在控制台中查看信息
通过上面示例可以看到使用Web Workers应该包括下面两部分:
☑ 定义主页线程
▶ 通过worker=new Worker(url)加载一个JavaScript文件,创建一个Worker,同时返回一个worker实例。
▶ 通过worker.postMessage(data)方法向worker发送数据。
▶ 绑定worker.onmessage事件接收worker响应的数据。
▶ 使用worker.terminate()可以终止一个worker执行。
☑ 定义Worker线程
▶ 通过postMessage(data)方法向主线程发送数据。
▶ 绑定onmessage事件接收主线程发送过来的数据。
【示例2】本示例演示如何创建Web Workers,手动控制Web Workers与页面进行通信的一般方法,同时设置如何处理异常,以及如何停止Worker任务处理。
首先,设计主页文件(index.html),并在该文件脚本中定义一个主线程。
然后,设计线程脚本文件(worker.js)的代码。
在主页和线程脚本文件中,分别使用addEventListener()方法把回调函数绑定到线程监听事件中。
最后,在Chrome浏览器中访问主页文件,单击“发送消息”按钮,可以在控制台中看到输出的信息,表示程序手动控制线程交互执行成功,如图26-2所示。
图26-2 在控制台中查看信息
使用addEventListener()方法注册后台线程的响应事件比较麻烦,当然也可以把它修改为下面这种传统写法。
☑ 主线程脚本(index.html)
☑ Worker线程文件(worker.js)
26.3 案例实战
本节将通过多个实例演示如何灵活应用Web Workers,实现并发式应用程序开发。
26.3.1 使用多线程实现后台运算
本节例设计一个文本框,允许用户在该文本框中输入数字,然后单击按钮,在后台计算从1到给定数值的和。虽然对于从1到给定数值的求和计算只需要用一个求和公式就可以了,但是本示例中为了展示后台线程的使用方法,采取循环计算的方法。
为了方便比较单线程与多线程的运算差异,首先采用传统方式设计一个单线程计算页面,页面代码如下:
保存页面,然后在浏览器中预览,执行上面这段代码,在文本框中输入数值,然后单击“计算”按钮。可以看到,在弹出提示对话框之前,用户是不能在该页面上执行操作的。虽然在文本框中输入比较小的值时,不会有什么延迟问题,但是当用户在该文本框中输入特别巨大的数字,如1亿以上的值时,浏览器会弹出提示对话框,提示脚本运行时间过长,如图26-3所示。
图26-3 Firefox浏览器提示脚本运行时间过长
重写该页面脚本,使用Web Workers把页面中比较耗时的运算放在后台运行,这样在上例的文本框中无论输入多么大的数值都可以正常运算。
【操作步骤】
第1步,设计主页面,在该页面中创建一个Worker,然后导入汇总计算的外部JavaScript文件。通过postMessage()方法将用户输入的数字传递给Worker,并通过onmessage事件回调函数接收运算的结果。
第2步,把对于给定值的求和运算放到线程中单独执行,且把线程代码单独存储在SumCalculate.js脚本文件中。
第3步,在支持Web Workers的浏览器中预览,如Firefox、Safari、Chrome、Opera等浏览器,在Firefox中的运行结果如图26-4所示。
图26-4 Firefox浏览器多线程运行效果
26.3.2 在后台过滤值
在Web应用中,建议用户把非即时性的任务处理放在后台实现,以减轻前台处理的压力。本示例设计在页面上随机生成一个整数的数组,然后将该整数数组传入线程,让后台帮助挑选出该数组中可以被3整除的数字,然后显示在页面表格中。读者可以借助这种设计思路,实现把字符串、数组、列表中的数据都采取该方法显示在页面表格、表单控件甚至统计图中。
【操作步骤】
第1步,设计前台页面代码,该页面的HTML代码部分包含一个空白表格,在前台脚本中随机生成整数数组,然后送到后台线程挑选出能够被3整除的数字,再传回前台脚本,在前台脚本中根据挑选结果动态创建表格中的行、列,并将挑选出来的数字显示在表格中。
第2步,将后台线程中需要处理的任务代码存放在脚本文件script.js中,详细代码如下:
第3步,在浏览器中预览,则运行结果如图26-5所示。
图26-5 在后台过滤值
26.3.3 多任务并发处理
利用线程可以嵌套的特性,可以在Web应用中实现多个任务并发处理,这样能够提高Web应用程序的执行效率和反应速度。同时通过线程嵌套把一个较大的后台任务切分成几个子线程,在每个子线程中各自完成相对独立的一部分工作。
本节示例将在26.3.2节示例基础上,把主页脚本中随机生成数组的工作放到后台线程中,然后使用另一个子线程在随机数组中挑选可以被3整除的数字。对于数组的传递以及挑选结果的传递均采用JSON对象来进行转换,以验证是否能在线程之间进行JavaScript对象的传递工作。
【操作步骤】
第1步,在主页面中定义一个线程。设计不向该线程发送数据,在onmessage事件回调函数中进行后期数据处理,并把返回的数据显示在页面中。
第2步,在后台主线程文件script.js中,随机生成200个整数构成的数组,然后把这个数组提交到子线程,在子线程中把可以被3整除的数字挑选出来,然后送回主线程。主线程再把挑选结果送回页面进行显示。
在上面代码中,向子线程中提交消息时使用的是worker.postMessage()方法,而向主页面提交消息时使用postMessage()方法。在线程中,向子线程提交消息时使用子线程对象的postMessage()方法,而向本线程创建源发送消息时直接使用postMessage()方法即可。
第3步,设计子线程的任务处理代码。下面是子线程代码,子线程在接收到的随机数组中挑选能被3整除的数字,然后拼接成字符串并返回。
在子线程中向发送源发送回消息后,如果该子线程不再使用的话,则应该使用close语句关闭子线程。
第4步,在主页面的主线程回调函数中处理后台线程返回的数据,并将这些数据显示在页面中。
第5步,此时在浏览器中预览,则会看到类似如图26-6所示的运行效果。
图26-6 多任务并发处理
26.3.4 在多线程之间通信
当主线程嵌套多个子线程时,子线程之间可以通过下面几个步骤进行通信:
第1步,先创建发送数据的子线程。
第2步,执行子线程中的任务,然后把要传递的数据发送给主线程。
第3步,在主线程接收到子线程传回来的消息时,创建接收数据的子线程,然后把发送数据的子线程中返回的消息传递给接收数据的子线程。
第4步,执行接收数据子线程中的代码。
本节示例继续在前面示例基础上,将创建随机数组的工作也放到了一个单独的子线程中,在该线程中创建随机数组,然后将随机数组传递到另一个子线程中进行能够被3整除的数字挑选工作,最后把挑选结果传递回主页面进行显示。
【操作步骤】
第1步,完成主页面的设计。包括HTML结构和CSS样式。在主页脚本中创建一个主线程,定义请求数据为空,在主线程响应事件onmessage回调函数中处理后台返回的处理数据,并把它们显示在页面中。
第2步,修改主线程中的代码。在主线程中定义一个子线程(发送数据),让其随机生成200个数字,并返回这个随机数组。在该子线程的回调函数中再定义一个子线程(接收数据),把接收到的随机数组传递给它,并接收该线程过滤后的数组。
第3步,在发送数据的子线程中创建了一个由200个整数构成的随机数组。然后把它转换为字符串并返回,最后关闭该子线程。
第4步,在接收数据子线程中对接收到的随机数组中挑选能被3整除的数字,然后拼接成字符串并返回。
26.3.5 使用线程技术计算Fibonacci数列
Fibonacci数列是比较经典的数学规律,它以递归的方法定义:
F0=0
F1=1
Fn=F(n-1)+F(n-2)(n>=2,n∈N*)
使用JavaScript实现Fibonacci数列运算的一般方法如下:
在Chrome浏览器中如果调用fibonacci(39);,则执行时间需要大约19097毫秒,而要计算40的Fibonacci数列时,浏览器就会罢工,直接提示脚本忙。
由于JavaScript是单线程执行的,在求数列的过程中浏览器不能执行其他脚本,UI渲染线程也会被挂起,从而导致浏览器进入假死状态。
现在,尝试使用Web Workers将数列计算过程放入一个新线程中,避免单线程计算所带来的问题。首先,定义主页文件。
在主页脚本中创建一个线程,把Fibonacci数列计算任务交给新线程来完成。
然后,在新线程文件中(fibonacci.js)输入下面代码:
在Chrome浏览器中访问主页文件,则可以在控制台中看到输出的信息,如图26-7所示。
图26-7 在控制台中查看信息
26.3.6 使用多线程绘图
JavaScript一个最原始的缺陷就是它的单线程任务处理,这个局限性意味着一个长时间运行的进程会冻结主窗口。我们常说浏览器UI线程阻塞,这是由于主线程在处理所有的可视化元素及其相关任务,如绘制、刷新、动画、用户输入事件等。
这种线程过载的严重后果是:页面被冻结,且用户不能再与Web应用进行交互。因此,Web应用的用户体验就非常差,用户可能赌气关掉这个Tab或者整个浏览器。为了避免发生这种情况,浏览器已经引入一种保护机制,当脚本运行时间过长,就会弹出警告。
但是,这种机制并不能正确分辨究竟一段脚本是编写有问题,还是它确实需要更多的时间来完成它的工作。尽管如此,由于它阻塞了UI线程,所以还是提示一下比让用户无限等待要好。
为了避免此类尴尬的情况,传统做法是通过setTimeout()和setInterval()方法,试图模拟并行任务。或者通过XMLHttpRequest对象,也可以异步地处理HTTP请求,避免从远程服务器载入资源时冻结UI。当然,现在有了Web Workers技术之后,情况会有很大的好转。
上面几节示例主要演示了Web Workers强化数学科学计算,如光线跟踪、分形、素数等。这些科学计算是理解Workers工作方式的很好示例,但是很少能给我们一些关于如何在真实世界的应用中使用它们的具体观点。当然,Web Workers还有很多新的有趣应用:
☑ 图像处理。通过从canvas或video元素中获取数据。可以把图像分割成几个不同的区域,并把它们推送给并行的不同Workers。这样就会在多线程中受益,此时运行速度明显加快。
☑ 大量数据检索。在调用XMLHTTPRequest处理大量数据时,如何处理这些数据所需的时间长短非常重要,最好在Web Worker中来做这些,避免冻结UI线程。这样可以保持一个可交互的应用。
☑ 背景数据分析。由于在使用Web Workers时,可以考虑JavaScript的新应用场景。例如,可以想象在不影响UI体验的情况下实时处理用户输入。利用这样一种可能,可以设计像Word (Office Web Apps套装)一样的应用,当用户打字时,后台在词典中进行查找,帮助用户自动纠错等。
☑ 针对本地数据的并发请求。提供本地存储(Local Storage)所不能提供的功能,针对Web Workers的线程安全的存储环境。
此外,如果在视频游戏世界,可以考虑将人工智能或者物理引擎的数据发送到Web Workers。一般来说,只要不需要DOM,任何可能影响用户体验的、耗时的JavaScript代码都可以使用Web Workers并发处理。然而,使用Web Workers时也需要注意3点:
☑ Worker初始化时间和通信时间不应该比自身的处理时间长。
☑ 使用多个Worker时,将会占用大量系统资源,消耗更多内存。
☑ 代码块之间的依赖关系,需要一些同步的逻辑,并行就没有那么简单。
本示例演示如何使用多线程绘制光线追踪的特效。光线追踪使用一些CPU密集型的数学计算,据此来模拟光线的路径。通过大量并发数学计算来模拟一些诸如反射、折射、材质等效果,演示效果如图26-8所示。
图26-8 光线追踪特效
如果不用多线程并发计算和渲染,则可以看到浏览器要完成整个图像绘制需要2秒多,如图26-9所示。而选用16个子线程并发计算,则浏览器会快速完成图像绘制,仅花费1秒多点,如图26-10所示。
本案例完整源代码请参考光盘示例,下面简单介绍两处最关键的代码。
图26-9 单线程处理
图26-10 16个线程处理
在主页面包含的jobqueue.js文件中,通过一个分支结构检测浏览器是否支持Web Worker,如果支持,则允许选择设置多线程项,并用多线程进行处理,否则只能够使用单线程处理任务。
为GO按钮绑定一个click事件处理函数,在该函数中使用for循环体创建多个子线程,实现并发数据计算,以提供图像绘制和渲染速度。
在子线程文件render-task.js中,尝试调用绘图、渲染函数完成图形绘制功能。
在这个应用脚本中,有两个方法明显地占用了大多数时间:renderScene()和getPixelColor()。
getPixelColor()方法的目的是计算当前的像素。光线追踪是一个像素一个像素地渲染场景。getPixelColor()方法之后再调用rayTrace()方法接管渲染阴影、环境光等操作。这是本案例应用的核心部分。并且如果分析rayTrace()方法的代码,这些代码没有任何DOM依赖。因此非常适合把它们设置为并行处理。此外,用户很容易将图像渲染拆分到几个线程中,由于每个像素的计算之间没有同步进行的必要。每个像素的操作与它们的邻居是独立的,因为在本例中没有使用抗锯齿。
使用浏览器预览这个光线追踪例子,可以看出不使用Worker和使用4个Worker的显著区别:
☑ 使用单线程的processRenderCommand()方法几乎占据了全部可用CPU,并且场景绘制耗时2.048秒。
☑ 使用16个Web Workers之后,processRenderCommand()方法在16个并行的线程中执行。甚至可以在右侧一栏看到它们的Worker Id。这次场景绘制耗时1.031秒。受益是真实存在的:场景绘制快了一倍。
26.4 综合案例:模拟退火算法
模拟退火算法来源于固体退火原理,将固体加温至充分高,再让其徐徐冷却,加温时,固体内部粒子随温升变为无序状,内能增大,而徐徐冷却时粒子渐趋有序,在每个温度都达到平衡态,最后在常温时达到基态,内能减为最小。
如果直接使用JavaScript完成模拟退火算法运算,则会造成卡机,甚至假死现象,Mozbox的Paul Rouget用模拟退火算法演示了Web Workers的优越性。下面就来分析如何使用Web Workers实现这个应用,案例演示效果如图26-11所示。
图26-11 模拟退火算法
26.4.1 认识模拟退火算法
认识模拟退火前,先了解爬山算法。爬山算法是一种简单的贪心搜索算法,该算法每次从当前解的临近解空间中选择一个最优解作为当前解,直到达到一个局部最优解。
爬山算法实现很简单,其主要缺点是会陷入局部最优解,而不一定能搜索到全局最优解。如图26-12所示,假设C点为当前解,爬山算法搜索到A点这个局部最优解就会停止搜索,因为在A点无论向那个方向小幅度移动都不能得到更优的解。
图26-12 爬山算法
爬山法是完完全全的贪心法,每次都鼠目寸光的选择一个当前最优解,因此只能搜索到局部的最优值。模拟退火其实也是一种贪心算法,但是它的搜索过程引入了随机因素。模拟退火算法以一定的概率来接受一个比当前解要差的解,因此有可能会跳出这个局部的最优解,达到全局的最优解。以图26-12为例,模拟退火算法在搜索到局部最优解A后,会以一定的概率接受到E的移动。也许经过几次这样的不是局部最优的移动后会到达D点,于是就跳出了局部最大值A。
模拟退火算法描述:
若J(Y(i+1))>= J(Y(i))(即移动后得到更优解),则总是接受该移动。
若J(Y(i+1))< J(Y(i))(即移动后的解比当前解要差),则以一定的概率接受移动,而且这个概率随着时间推移逐渐降低(逐渐降低才能趋向稳定)。
这里一定的概率计算参考了金属冶炼的退火过程,这也是模拟退火算法名称的由来。
根据热力学的原理,在温度为T时,出现能量差为dE的降温的概率为P(dE),表示为:
P(dE)=exp(dE/(kT))
其中k是一个常数,exp表示自然指数,且dE<0。这条公式说白了就是:温度越高,出现一次能量差为dE的降温的概率就越大;温度越低,则出现降温的概率就越小。又由于dE总是小于0(否则就不叫退火了),因此dE/kT < 0,所以P(dE)的函数取值范围是(0,1)。
随着温度T的降低,P(dE)会逐渐降低。将一次向较差解的移动看作一次温度跳变过程,以概率P(dE)来接受这样的移动。
关于爬山算法与模拟退火,有一个有趣的比喻:爬山算法是兔子朝着比现在高的地方跳去。它找到了不远处的最高山峰,但是这座山不一定是珠穆朗玛峰。这就是爬山算法,它不能保证局部最优值就是全局最优值。模拟退火是兔子喝醉了,它随机地跳了很长时间。这期间,它可能走向高处,也可能踏入平地,但是它渐渐清醒了并朝最高方向跳去。这就是模拟退火。
26.4.2 编写应用主页面
下面代码是调用Web Workers的HTML主页面代码。为了方便说明,该HTML示例也化繁为简。
目的不在于建立漂亮的界面,而是通过搭建一个简洁的框架,演示如何控制Web Workers并实际运行。
应用程序的页面嵌入了canvas元素来显示输入的图像。页面上还有一个按钮,包括开始执行运算的按钮,以及一个复选框,决定是否采用Web Workers多线程模式。
接下来在main.js文件中编写GO按钮的click事件响应函数,该函数以controls.go()方法呈现,调用该方法将初始化页面,并创建6个Test实例,完成6幅图像的绘制。
26.4.3 编写Worker.js
现在,将Worker用来与页面通信的代码添加到Worker.js文件中,详细代码如下。Web Workers完成运算即可使用postMessage通知页面。将利用这个信息更新主页上的图像。Worker创建完成后,会先使用importScripts(‘annealing.js’);命令导入退火算法文件annealing.js,然后实例化一个退火算法对象,在Worker线程的onmessage回调函数中执行退火运算,并返回运算结果。
26.4.4 与Web Worker通信
在index.html文件中,可以通过给Worker发送一些代表运算任务的数据和参数来使用Worker。方法是使用postMessage()函数发送一个JavaScript对象。这个JavaScript对象包含了Worker负责处理的RGBA图像数据阵列、图像的尺寸和像素范围。每个Worker根据接收到的信息分别对图像的不同部分进行处理,然后在canvas.js中通过canvas元素来显示输入的图像数据。
postMessage可以对imageData对象进行高效序列化,以便通过canvas API使用。不过一些支特Worker和postMessage API的浏览器也许还不能支持postMessage的这种扩展的序列化能力。例如,Firefox 3.5不能通过postMessage传送imageData对象,不过未来版本可能会提供此支持。因此,在本章中介绍的图像处理示例中,以传送imageData.data(数据序列化方式间JavaScript数组一样)的方式代替传送imageData对象本身在Web Workers执行运算任务时,它们同时将其状态和结果返回到页面上。同样地,消息包含了一个JavaScript对象,这个对象中含有图像数据和标记处理范围的坐标信息。
HTML页面上的消息处理程序接收上述数据,并用新的像素值更新canvas。处理后的图像数据到达时,HTML页面即时显示结果。现在,我们创建了一个可以处理图像的示例应用,并且具有利用多核CPU的潜在优势。此外Web Workers执行时,程序也不会锁定用户界面,不会让用户界面停止响应。