作者:jolamjiang,腾讯WXG前端开发工程师一篇关于WebWorker、SharedArrayBuffer、Atomics的文章。为什么需要多线程编程?当你看到文章标题《Javascript 多线程编程》的时候,你可能马上会有疑问:Javascript不是单线程的吗?JavascriptIO阻塞等异步需求(如setTimeout、Promise、requestAnimationFrame、queueMicrotask等)不是通过事件循环(EventLoop)来解决的吗?是的,Javascript确实是单线程的,阻塞等异步需求确实是通过实现循环来解决的,但是当线程需要处理大规模计算时,这种机制就不起作用了。非常适用,试想一下场景:你需要实现文件的加密和解密。你的VirtualDom树有很多元素(比如几万个),你需要对这棵树进行Diff操作。您需要在浏览器中“挖掘”。以上场景都会阻塞主线程,也就是当你执行这些操作的时候,你的页面卡住了。页面卡住一段时间后,Chrome等浏览器或操作系统会建议你杀掉整个Tab或进程。这显然不是我们希望看到的。因为这些场景的存在,浏览器提出W3C在2013年提出了WebWorker草案,就是为了解决上述问题而提出的。为了让大家感受一下JS多线程到底能干什么,作者基于WebWorker(线程)、ShareArrayBuffer(共享)等WebAPI写了一个前端压缩和解压文件(基于DEFLATE算法)的demomemory)和Atomics(锁):WebWorkerChrome浏览器中的每个Tab都是一个进程,每个进程都会有一个主线程,网页的渲染(Style、Layout、Paint、Composite)都会在主线程上进行操作.主线程可以发起多个WebWorker,WebWorkers对应“线程”的概念。每个WebWorker对应一个脚本文件。主线程可以通过以下代码启动多个WebWorker,并通过基于事件的API与WebWorker进行通信:main.jsletworker=newWorker("work.js");worker.postMessage("HelloWorld");worker。onmessage=function(event){console.log("Receivedmessage"+event.data);}WebWorker也是通过相应的实现APIworker.jsthis.addEventListener("message",function(e){this.postMessage("你说:"+e.data);},false);WebWorker通信效率和同步问题主线程通过postMessage(data:any)与WebWorker通信,此时会先复制数据,再传递给WebWorker;同理,WebWorker通过postMessage(data:any)与主线程通信时,也会先复制数据,再传递给主线程。这样做显然会导致沟通效率问题。想象一下,你需要在WebWorker中解压一个1G的文件。您需要将整个1G文件复制到WebWorker。WebWorker解压1G文件后,会将解压后的文件复制回主线程。SharedArrayBuffer为了解决通信效率的问题,浏览器提出了ShareArrayBuffer,它基于ArrayBuffer和TypedArrayAPI。ArrayBuffer对应一块内存(二进制内容)。为了操作这块内存,浏览器需要提供一些视图(Int8Array等)。有符号数的数组。注意:将ArrayBuffer中的二进制流翻译成各种视图时,是小端还是大端由具体硬件决定。在大多数情况下,使用小端字节顺序。这段内存可以在不同的Worker之间共享,但是内存的共享会带来另外一个问题,即竞争(raceonditions)的问题:当计算机指令对内存操作进行操作时,我们可以把它看成是一个二-步骤过程。:一种是从内存中取值,另一种是计算赋值给某块内存。当我们有两个线程对同一个内存地址进行+1操作时,假设线程是顺序运行的,为了简化模型,我们可以如下图表示:上面两个线程的运行结果也满足我们的预期,也就是线程都对同一个地址进行+1操作,最后得到结果3。但是因为两个线程是同时运行的,所以经常会出现下图所示的问题,即读写可能不会在一次事务中发生:这种情况称为竞争条件。Atomics为了解决上述竞争问题,浏览器提供了AtomicsAPI,它是一组可以绑定读写的原子操作。比如下图中S1到S3的操作,被浏览器封装为Atomics.add()这个API,从而解决了race问题。AtomicsAPI具体包括:Atomics.add()Atomics.and()Atomics.compareExchange()Atomics.exchange()Atomics.isLockFree()Atomics.load()Atomics.notify()Atomics.or()Atomics.store()Atomics.sub()Atomics.wait()Atomics.xor()有了这套API,我们就可以在Golang中实现类似GolangSynchronizationPrimitives的功能。下面介绍Mutex和Cond的实现。WebAssembly具备SharedArrayBuffer和Atomics能力后,证明浏览器可以提供内存共享和锁的实现,这意味着WebAssembly线程在浏览器机制中可以得到高效保障。其实我很怀疑SharedArrayBuffer和Atomics顺便给JSRuntime提供API是为了支持WebAssembly,因为到目前为止我还没有看到ES对锁有比较丰富的草稿(比如Java中的synchronized关键字)。Mutext和Cond的实现上面已经说了。GolangSynchronizationPrimitives等API可以基于ShareArrayBuffer和Atomics开发。下面介绍一下Mutex和Cond的实现。实现的介绍是基于MozzilaJavascript编译器工程师LarsTHansen对锁库的实现。Mutex先说Mutex的作用。Mutex的API大概是这样的:letmutex=newLock(shareArrayBuffer,...);mutex.lock();doSomething();mutex.unlock();Mutex可以保证lock()和unlock()之间的code码不会被打断。下面是具体实现的介绍:首先定义Mutex的三种状态以及对应的状态机UNLOCK:解锁LOCKED:锁定WAITED:锁定且大于等于1个线程正在等待锁对于Mutex的每个状态为工作线程可能是初始状态,状态到状态的反转会产生一些操作,进入下一个状态:locklock()初始状态为UNLOCK:锁未被抢占,状态反转为LOCKED,线程执行后续操作。初始状态为LOCKED:锁已经被抢占,状态反转为WAITED,线程设置为等待状态,设置为当锁的状态不是WAITED时唤醒线程。一旦被唤醒,线程拥有锁,线程执行后续操作。初始状态为WAITED:锁已被抢占,线程设置为等待状态,设置为非WAITED锁状态时唤醒线程。一旦被唤醒,该线程就拥有了锁,该线程将执行后续操作。Releaseunlock()1.初始状态为LOCKED:锁被抢占不等待,状态反转为UNLOCK,线程进行后续操作。2、初始状态为WAITED:锁被抢占等待,状态反转为LOCKED,唤醒一个处于等待状态的线程,该线程执行后续操作。对应上述逻辑的代码如下://lockLock.prototype.lock=function(){constiab=this._iab;conststateIdx=this._ibase;letc;if((c=Atomics.compareExchange(iab,stateIdx,0,1))!=0){do{if(c==2||Atomics.compareExchange(iab,stateIdx,1,2)!=0)Atomics.wait(iab,stateIdx,2);}while((c=Atomics.compareExchange(iab,stateIdx,0,2))!=0);}}//unlockLock.prototype.unlock=function(){constiab=this._iab;conststateIdx=this._ibase;letv0=原子。sub(iab,stateIdx,1);//Wakeupawaiterifthereareanyif(v0!=1){Atomics.store(iab,stateIdx,0);Atomics.notify(iab,stateIdx,1);}}可以看到实现lockAtomics.compareExchange()和Atomics.wait()(相当于Linux中的futex)是两个原子操作。CondCond是基于Mutex实现的,它的一般作用是在持有锁的同时执行两个操作:wait():本线程的进程进入等待状态,被唤醒时再次持有锁。notifyOne():唤醒一个正在等待的线程。具体使用方法如下://threadAvarmsg=newInt32Array(sab,msgLoc,1);lock.lock();while(msg[0]
