使用屏幕外画布终止多个 Web Worker 时出现问题

问题描述 投票:0回答:1

我是一个使用网络工作者的初学者,我正在处理一个小问题。

我正在创建几个工作人员来处理音频缓冲区并在屏幕外画布上绘制其波形:

主线:

// foreach file
 let worker = new Worker('js/worker.js');
 let offscreenCanvas = canvas.transferControlToOffscreen();
 
 worker.addEventListener('message', e => {
    if (e.data == "finish") {
           worker.terminate();
        }
    });

 worker.postMessage({canvas: offscreenCanvas, pcm: pcm}, [offscreenCanvas]);
// end foreach

工人:

importScripts('waveform.js');

self.addEventListener('message', e => {
    let canvas = e.data.canvas;
    let pcm = e.data.pcm;
    
    displayBuffer(canvas, pcm); // 2d draw function over canvas
    self.postMessage('finish');
});

结果很奇怪。线程在 displayBuffer() 完成时立即终止,但正如您在分析中看到的那样,GPU 仍在渲染画布,这有时会导致渲染崩溃。没有错误,只有黑色画布。

我正在 Chrome 83.0 上运行

multithreading canvas web-worker offscreen-canvas
1个回答
2
投票

这是可以预料的,“提交”到主线程不是同步完成的,但是当浏览器适当地调暗它时(即通常在下一个绘画帧),所以当你调用

worker.terminate()
时,实际的绘画可能还没有发生,并且永远不会。

这是好奇的现场重现:

const worker_script = `
self.addEventListener('message', (evt) => {
  const canvas = evt.data;
  const ctx = canvas.getContext( "2d" );
  // draw a simple grid of balck squares
  for( let y = 0; y<canvas.height; y+= 60 ) {
    for( let x = 0; x<canvas.width; x+= 60 ) {
      ctx.fillRect(x+10,y+10,40,40);  
    }
  }
  self.postMessage( "" );
});`;
const worker_blob = new Blob( [ worker_script ], { type: "text/javascript" } );
const worker_url = URL.createObjectURL( worker_blob );
const worker = new Worker( worker_url );
worker.onmessage = (evt) => worker.terminate();
const canvas_el = document.querySelector( "canvas" );
const off_canvas = canvas_el.transferControlToOffscreen();
worker.postMessage( off_canvas, [ off_canvas ] );
<h3>Run this snippet a few times (in Chrome), sometimes it will work, sometimes it won't.</h3>
<canvas width="500" height="500"></canvas>

为了规避这个问题,曾经有一个 OffscreenCanvasRendering2DContext.commit() 方法,您可以在终止工作线程之前调用该方法,但是 我们正在删除它(稍后会处理这种边缘情况) .

if( !( 'commit' in OffscreenCanvasRenderingContext2D.prototype ) ) {
  throw new Error( "Your browser doesn't support the .commit() method," +
    "please enable it from chrome://flags" );
}
const worker_script = `
self.addEventListener('message', (evt) => {
  const canvas = evt.data;
  const ctx = canvas.getContext( "2d" );
  // draw a simple grid of balck squares
  for( let y = 0; y<canvas.height; y+= 60 ) {
    for( let x = 0; x<canvas.width; x+= 60 ) {
      ctx.fillRect(x+10,y+10,40,40);  
    }
  }
  // force drawing to element
  ctx.commit();
  self.postMessage( "" );
});`;
const worker_blob = new Blob( [ worker_script ], { type: "text/javascript" } );
const worker_url = URL.createObjectURL( worker_blob );
const worker = new Worker( worker_url );
worker.onmessage = (evt) => worker.terminate();
const canvas_el = document.querySelector( "canvas" );
const off_canvas = canvas_el.transferControlToOffscreen();
worker.postMessage( off_canvas, [ off_canvas ] );
<h3>Run this snippet a few times (in Chrome), it will always work ;-)</h3>
<canvas width="500" height="500"></canvas>

因此,如果没有此方法,一个解决方法是在终止 Worker 之前等待。虽然似乎没有精确的时间量,也没有我们可以等待的特殊事件,但通过测试和错误,我等待了三个绘画帧,但这可能无法在所有设备上执行,因此您可以为了最安全,请等待几秒钟,甚至只是让垃圾收集器来处理它:

const worker_script = `
self.addEventListener('message', (evt) => {
  const canvas = evt.data;
  const ctx = canvas.getContext( "2d" );
  // draw a simple grid of balck squares
  for( let y = 0; y<canvas.height; y+= 60 ) {
    for( let x = 0; x<canvas.width; x+= 60 ) {
      ctx.fillRect(x+10,y+10,40,40);  
    }
  }
  self.postMessage( "" );
});`;
const worker_blob = new Blob( [ worker_script ], { type: "text/javascript" } );
const worker_url = URL.createObjectURL( worker_blob );
const worker = new Worker( worker_url );


worker.onmessage = (evt) =>
  // trying a minimal timeout
  // to be safe better do setTimeout( () => worker.terminate(), 2000 );
  // or even just let GC collect it when needed
  requestAnimationFrame( () => // before next frame
    requestAnimationFrame( () => // end of next frame
      requestAnimationFrame( () => // end of second frame
        worker.terminate()
      )
    )
  );
const canvas_el = document.querySelector( "canvas" );
const off_canvas = canvas_el.transferControlToOffscreen();
worker.postMessage( off_canvas, [ off_canvas ] );
<h3>Run this snippet a few times (in Chrome), it should always work.</h3>
<canvas width="500" height="500"></canvas>

虽然最好从

ImageBitmap
传输
OffscreenCanvas
并在主线程上提供位图渲染器上下文:

const worker_script = `
self.addEventListener('message', (evt) => {
  const { width, height } = evt.data;
  const canvas = new OffscreenCanvas(width, height);
  const ctx = canvas.getContext("2d");
  // draw a simple grid of black squares
  for (let y = 0; y<canvas.height; y+= 60) {
    for (let x = 0; x<canvas.width; x+= 60) {
      ctx.fillRect(x+10,y+10,40,40);  
    }
  }
  const bmp = canvas.transferToImageBitmap();
  self.postMessage(bmp, [bmp]);
});`;
const worker_blob = new Blob( [ worker_script ], { type: "text/javascript" } );
const worker_url = URL.createObjectURL( worker_blob );
const worker = new Worker(worker_url);
const canvas_el = document.querySelector("canvas");
const renderer = canvas_el.getContext("bitmaprenderer");

worker.onmessage = ({ data }) => {
  renderer.transferFromImageBitmap(data);
  worker.terminate();
}
const { width, height } = canvas_el;
worker.postMessage({ width, height});
<h3>Run this snippet a few times (in Chrome), it should always work.</h3>
<canvas width="500" height="500"></canvas>

现在,我应该注意到,为单个作业创建一个新的 Worker 通常是一个非常糟糕的设计。启动一个新的 js 上下文是一项非常繁重的操作,而使工作线程与主线程的 GPU 指令的链接是另一项操作,我不太了解你在做什么,但你应该真正考虑一下是否会这样做。不需要重用 Worker 和 OffscreenCanvas,在这种情况下,您应该让它们保持活动状态。

© www.soinside.com 2019 - 2024. All rights reserved.