内容提要:
- 什么是WebWorkers
- 为什么要有WebWorkers
- WebWorkers的能力
- 怎么使用WebWorkers
- Dedicated Worker
- Shared Worker
- 开发者工具支持
- 展望
正文:
Web Workers[1]是W3C和WHATWG定义的Web多线程标准, 是HTML5平台的重要组成部分。使用Web Workers API,HTML5程序员可以构建多线程化的应用,对于提升原有应用的性能,改善用户体验有非常重要的意义。应用级的多线程接口在Android,iOS,Windows,QT等平台上都是非常重要的一部分,从这个意义上看,Web Workers也是HTML5平台化的重要一环。
Web Workers在W3C的演进,在2012年5月已经到了Candidate Recommendation阶段,得到主流浏览器的广泛支持,详见这里。
为什么要有Web Workers?在笔者看来,主要有几个原因:
- 这是个多核的世界。服务器端自不用说,从个人电脑,到手机平板,多核心已经是主流。Web上提供多线程编程接口,才能通过在应用层对任务细分,充分利用硬件的能力。
- Web / JavaScript本身是单线程的。JavaScript本身只提供单线程的执行环境,没有多线程的语义。Web从出生开始,也默认很多工作都要在一个UI线程中完成,这包括页面解析,DOM的操作,CSS的计算,布局,页面渲染等业务逻辑,UI线程非常繁忙,导致性能差,不流畅。随着浏览器的演进,在引擎层面上,Web 引擎提供了多线程的页面解析,多线程渲染加速,Javascript引擎也提供了多线程编译,多线程垃圾回收等机制,这些将一部分业务从UI线程中剥离,通过并行来加速。在应用层面上,也提供异步的编程接口,例如异步xmlhttprequest, 以及setTimeout, setInterval等接口,尽可能的使UI线程不被阻塞。但这些还是不够,随着HTML5应用越来越复杂,出现越来越多的将应用的业务逻辑也多线程化的需求。例如:
- )在worker线程对用户的输入做拼写检查,从而不影响UI线程的响应性。
- )将任务分配到多个线程中,并行计算,加速渲染,例如分形算法。
- )分析audio或者video数据等密集计算。
- )本地的Web database的更新。
- )应用的资源预取和缓存。
Web Workers API定义了两种worker,一种是Dedicated Worker, 另外一种是Shared Worker。Dedicated Worker只能在他被创建的Context中访问。而Shared Worker一经创建,可以在同一个域里面的多个Context共享访问,例如不同的frame,不同的page。
Web Workers的能力
原则上,worker不具备任何直接影响页面的能力,例如DOM操作。而是需要通过postMessage通知UI线程,间接的影响页面。这种设计的思想和主流的多线程UI设计思想也是一致的。Worker具备的能力:
- navigator对象
- XMLHttpRequest
- setTimeout()/clearTimeout() and setInterval()/clearInterval()
- importScripts()
- performance对象
Worker不具备的能力:
- DOM
- window对象
- document对象
通过Chrome开发者工具,可以查看Worker具备哪些能力。
接下来将介绍这两种worker的主要API。
Dedicated Worker
如何创建一个Dedicated Worker
var worker = new Worker(“workerScript.js”);
上述语句会启动一个新的线程,下载workerScript.js并执行,注意,此处将是一个全新的执行环境,和创建worker的环境完全独立。
如何和一个Dedicated Worker通信
在UI线程向worker发送消息:
worker.postMessage(any message, optional sequence<Transferable> transfer);
这里会将message对象发送给worker线程,如果第二个参数transfer为空,message对象将会被序列化,而后传递到worker线程,worker线程反序列化,构造出message对象。这个过程是完全的拷贝。
第二个参数transfer用来指明哪些要避免拷贝,而是要transfer所有权到另外一个线程。以transfer列表包含Transferrable Object obj为例,这意味着message中的obj要transfer到worker线程;那么postMessage之后,UI线程的JavaScript执行环境将不再拥有obj,而Worker线程会拥有obj,这类似C++ STL里面的auto_ptr。ArrayBuffer是一种常见的可传递对象,可以支持大数据在线程之间传递,从而避免了拷贝。目前所支持的Transferrable Object见这里。
关于transfer列表,除了以上之外,2015年刚刚开始提案的新标准SharedArrayBuffer里面定义的SharedArrayBuffer这个类型,可以在多个线程之间共享,这类似于C++ STL里面的shared_ptr。
在worker线程接收消息:
onmessage = function(message) {
…
}
在workerScript.js里实现onmessage函数,这里用来接收从主线程发过来的消息。
如何从Dedicated Worker线程发送消息到UI线程
在worker的执行环境里
postMessage(any message, optional sequence<Transferable> transfer);
语义同从UI线程向worker线程传递消息。
在UI线程里接收消息
worker.onmessage = function (message) {
…
}
注册onmessage回调函数到worker对象,可以接收worker线程传递过来的消息。
如何停止worker线程的执行
如果在UI线程停止worker运行
worker.terminate();
worker线程在隐式地收到terminate消息后,消息队列里的未处理的消息会被丢掉,不再被onmessage函数处理。
如果在worker线程停止运行,则可以调用方法:
close();
close被调用后,所有已经在worker线程的消息队列里的消息都会被丢掉,不再被onmessage处理。
错误处理
worker.onerror = function(error) {
…
}
在UI线程,注册onerror回调函数到worker对象,那么worker线程在有runtime error发生的时候,会发error事件给主线程。
示例
下面是一个简单的例子,用来描述Dedicated Worker的使用。主线程创建worker线程,worker用来计算素数,并且返回给UI线程做显示。
Shared Worker
如何创建一个Shared Worker
var worker = new SharedWorker(“service.js”);
上述语义,在同一个origin范围内,如果尚未存在以service.js创建的shared worker,则会创建新的线程,下载service.js,在一个新的执行环境中运行,并且在UI线程返回SharedWorker对象,这个对象会带一个MessagePort[2],用于和shared worker通信。
如果已经有以service.js创建的shared worker,则不再创建新的线程,而是在UI线程返回一个SharedWorker对象,可以和shared worker通信。此处是以SharedWorker构造函数的参数作为区分,不同的URL创建不同的shared worker线程,相同则共享同一个shared worker。另外,SharedWorker只能在与它相同的origin的执行环境中被访问。
UI 线程如何向Shared Worker发消息
SharedWorker构造函数返回的对象会带一个MessagePort,用来和worker通信。
首先,建立和SharedWorker的连接,在 UI 线程上worker的onmessage函数注册,或者显示的调用MessagePort的start方法。
worker.port.onmessage = function cb(…) { }
或者
worker.port.addEventListener(function cb(…){ });
worker.port.start();
这样,SharedWorker线程的onconnect回调函数会被调用,这个回调函数的参数中可以拿到对应UI线程对应的MessagePort,然后再在这个port上注册onmessage回调函数,就准备好从UI 线程上接收消息了。例如:
onconnect = function(e) {
var port = e.ports[0];
port.onmessage = function()…
}
再然后,UI线程上可以用postMessage方法向SharedWorker发消息,这里和Dedicated Worker的语义是一样的,不同的是,Transferable Object在Shared Worker里并不一定被支持。
Shared Worker如何向UI 线程发消息
接上,在SharedWorker在onconnect的回调函数里拿到MessagePort之后,就可以通过这个port和对应的UI线程里的对象通信了。
port.postMessage(…);
如何停止Shared Worker线程的执行
Shared Worker线程可以通过调用close方法停止worker。
和Dedicated Worker有所不同,UI线程里的shared worker对象并没有terminate方法。
错误处理
同Dedicated worker
示例
这个示例演示了Shared worker和UI线程连接之后,shared worker会向UI线程发送‘Hello World‘消息。
开发者工具支持
Dedicated Worker由于总是和创建它的执行环境关联,所以在页面调试的Source面板同样可以进行worker调试。如下图,选择要调试的线程,并且给该线程的JavaScript代码添加断点。例如:
而对于Shared Worker的调试,则要在独立的调试环境下,具体步骤如下:
- Chrome地址栏输入chrome://inspect并回车。
- 在inspect页面左侧列表选中Shared Workers,然后点击inspect链接
- 然后会跳出新的Chrome开发者工具窗口,可以进行shared worker调试。如下图所示:
展望
W3C Web Workers标准,在应用层面线程化,利用硬件的多核能力提高性能,改善用户体验起到非常重要的作用。但由于JavaScript语言天然地缺乏线程语义,编程接口也只能设计为消息传递,而非变量共享。随着HTML5的发展,有越来越多的场景需要在UI线程以及多个worker线程之间共享数据,为此,业界也开始提出新的标准提案SharedArrayBuffer[3],在这个新提案中不仅增加了共享的数据类型,而且在此之上定义了原子操作的语义和线程同步的语义,进一步缩小了HTML5平台和传统原生应用在多线程能力上的差距。
更多相关内容,请参考
[1] https://www.w3.org/TR/workers/
[2] https://www.w3.org/TR/2011/WD-webmessaging-20110317/#message-ports
[3] https://tc39.github.io/ecmascript_sharedmem/shmem.html
作者简介:
邓攀
资深码农,英特尔亚太研发有限公司Web技术和优化中心,长期耕耘于Web领地