当前位置: 首页 > Web前端 > JavaScript

JS多线程并发

时间:2023-03-27 17:35:22 JavaScript

为什么需要并发我们经常听说JS是单线程模型,即所有代码都在主线程中执行。如果某些任务计算量大,会导致主线程阻塞,轻则UI界面掉帧,重则卡顿。//在任意web控制台执行以下代码,页面会卡住3sfunctionexecTask(){constt=performance.now()//模拟耗时任务while(performance.now()-t<3000){}}execTask()所以在计算量大的场景下,JS需要支持并发能力,避免阻塞主线程,影响用户体验。并发面临的问题用一个很简化的例子来说明并发面临的问题:10个线程同时执行1000个任务,如何避免某个任务被重复执行?方法一:任务列表对线程不可见,而是新开一个线程统一分配任务,收集其他线程的执行结果。方法二:任务列表对所有线程可见(共享内存),线程先排队接收任务号,然后执行对应号的任务。延伸阅读并发问题如何在JS中实现以上两种方法JS使用WebWorkerAPI来实现多线程并发。分配任务,多个Worker执行functionworkerSetup(){self.onmessage=(evt)=>{constt=performance.now()//模拟耗时任务,随机消耗时间0~100mswhile(performance.now()-t{constblob=newBlob([`(${workerSetup.toString()})()`])consturl=URL.createObjectURL(blob)returnnewWorker(url)}//模拟1000个任务consttasks=Array(1000).fill(0).map((_,idx)=>idx+1)constresult=[]letrsCount=0constonMsg=(evt)=>{result[evt.data.idx]=evt.data.valrsCount+=1//当所有任务完成时打印结果if(rsCount===tasks.length){console.log('task:',tasks)console.log('result:',result)}}//模拟线程池constworkerPool=Array(10).fill(0).map(createWorker)workerPool.forEach((worker,idx)=>{worker.onmessage=onMsgworker.id=idx})for(constidxintasks){//随机分配任务constworker=workerPool[Math.floor(Math.random()*workerPool.length)]worker.postMessage({idx,val:tasks[idx]})console.log(`Worker${worker.id},processtask${idx}`)}Multi-Workersharedtask(memory)SharedArrayBuffer是JS唯一提供的不同线程间共享内存的方式2018年1月5日禁用SharedArrayBuffer2020年新,安全方法已标准化以重新启用SharedArrayBuffer。需要设置两个HTTP消息头来跨域隔离您的站点:首先确保SharedArrayBuffer可用。代码可以复制到本页面的控制台执行测试::functionworkerSetup(){functionexecTask(val){constt=performance.now()//模拟耗时任务,随机耗时0~100mswhile(performance.now()-t{const{idx,sab}=evt.dataconstuint16Arr=newUint16Array(sab)while(true){//模拟排队接收任务//如果使用taskNo=uint16Arr[0]获取任务号,会出现抢任务(重复任务)的现象consttaskNo=Atomics.add(uint16Arr,0,1)if(taskNo>=uint16Arr.length)break//每个任务写入不同的位置,所以不需要原子操作uint16Arr[taskNo]=execTask(uint16Arr[taskNo])console.log(`Worker${idx},处理任务${taskNo}`)}self.postMessage(true)}}constcreateWorker=()=>{constblob=newBlob([`(${workerSetup.toString()})()`])consturl=URL.createObjectURL(blob)returnnewWorker(url)}//第一位存放下一个任务编号,后面的1000存放对应的任务和结果constsab=newSharedArrayBuffer((1+1000)*2)constuint16Arr=newUint16Array(sab)uint16Arr[0]=1for(leti=1;i{rsCount+=1if(rsCount===workerPool.length){console.log('result:',uint16Arr,sab)}}workerPool.forEach((worker,idx)=>{worker.onmessage=onMsgworker.postMessage({idx,sab})})Atomics对象提供了一组静态方法对SharedArrayBuffer对象进行原子操作task)处理1000个任务,调用postMessage2000次(任务分配,反馈结果),即数据在两个worker之间传递,经历了2000次结构化克隆。一般来说,结构化克隆速度更快,影响也很小。即使在最慢的设备上,您也可以将最大100KiB的postMessage()对象保持在100毫秒的响应预算内。如果您有JS驱动的动画,则高达10KiB的有效负载是无风险的。这对大多数应用程序来说应该足够了。即使在非常慢的设备上,您也可以使用postMessage()传递100KiB对象,保证在100毫秒内响应。如果有JS驱动的动画,传递10KiB的数据是没有风险的。这对于大多数应用程序应该足够了。另外,有些native对象是Transferable对象,postMessage(arrayBuffer,[arrayBuffer])不需要clone就可以转移这些对象的所有权。目前实现Transferrable的对象有:ArrayBuffer、MessagePort、ReadableStream、WritableStream、TransformStream、AudioData、ImageBitmap、VideoFrame、OffscreenCanvas、RTCDataChannel,所以优先使用该方法。方法二(共享内存)共享内存(SharedArrayBuffer)节省了线程间通信的消耗,但增加了代码的复杂度,只能共享二进制数据,ShareArrayBuffer与Atomics存在一定的兼容性问题。(目前没遇到必须用SharedArrayBuffer的场景,只看到用了WASM软解HEVC)其他JS中的其他方法可以在其他线程/进程中执行代码。ClusterCluster文档工作进程使用child_process.fork()方法进行分叉,因此它们可以通过IPC与父进程通信并来回传递服务器句柄。尽管node:cluster模块的主要用例是网络,但它也可以用于需要工作进程的其他用例。多进程,一般用于在Node.js上运行WEB服务器。集群共享端口有点像个笑话worker-threadsworker-threadsdocumentationWorkerimplementationonNode.js。工作线程对于执行CPU密集型JavaScript操作很有用。它们对I/O密集型工作不是很有帮助。Node.js内置的异步I/O操作比工作线程更高效。不同于child_process或cluster,worker_threads可以通过传递ArrayBuffer实例或共享SharedArrayBuffer实例来实现共享内存。WorkletsWorklets是在特定场景下使用的,非通用的多线程能力。Worklet接口是WebWorkers的轻量级版本,使开发人员能够访问渲染管道的低级部分。使用Worklets,您可以运行JavaScript和WebAssembly代码以进行需要高性能的图形渲染或音频处理。PaintWorklet自定义css绘制行为AudioWorklet用于自定义AudioNodes的音频处理