【问题标题】:Getting progress from web-worker executing computationally intensive calculation从执行计算密集型计算的网络工作者那里获得进展
【发布时间】:2020-07-13 10:22:22
【问题描述】:

我让 WebWorker 进行计算密集型递归计算,持续几秒钟。我想每 500 毫秒将进度消息发布到父线程(主窗口)。

我尝试使用setInterval 来实现这一点。但是由于线程被主计算阻塞,setInterval在那段时间根本没有执行。

网络工作者代码:

    // global variable holding some partial information
    let temporal = 0;
        
    // time intensive recursive function. Fibonacci is chosen as an example here.
    function fibonacci(num) {
        // store current num into global variable
        temporal = num;
      
      return num <= 1
        ? 1
        : fibonacci(num - 1) + fibonacci(num - 2);
    };

    self.onmessage = function(e) {
        // start calculation
        const result = fibonacci(e.data.value);
        postMessage({result});
    }
  
  setInterval(function() { 
    // post temporal solution in interval.
    // While the thread is blocked by recursive calculation, this is not executed
    postMessage({progress: temporal});
  }, 500);

主窗口代码

  worker.onmessage = (e) => { 
    if (e.data.progress !== undefined) {
      console.log('progress msg received')
    } else {
      console.log('result msg received')
      console.log(e.data)
    }
  };

  console.log('starting calculation');
  worker.postMessage({
    'value': 42,
  });

参见 jsFiddle 示例 - https://jsfiddle.net/m3geaxbo/36/

当然,我可以在fibonacci 函数中添加一些代码来计算经过的时间并从那里发送消息。但我不喜欢它,因为它会用不相关的代码污染函数。

    function fibonacci(num) {
        // such approach will work, but it is not very nice.
        if (passed500ms()) {
            postMessage({progress: num})
        }
      
      return num <= 1
        ? 1
        : fibonacci(num - 1) + fibonacci(num - 2);
    };

有没有首选的方法,如何在不污染执行计算本身的代码的情况下获得密集的 web-worker 计算的进度?

【问题讨论】:

    标签: javascript web-worker


    【解决方案1】:

    如果没有在内部集成某种让步,就无法让您的算法同步执行。 你必须调整你的算法,以便你可以暂停它,并检查是否有足够的时间过去,甚至让事件循环真正循环。

    让事件循环执行其他任务是我个人的最爱,因为它还允许主线程与 Worker 通信,但是,如果你真的只是想让它详细说明当前进度,一个简单的同步时间检查刚刚好。

    请注意,递归函数本质上在这种情况下并不真正可用,因为函数将在第 5 个嵌套级别生成的值不会反映您通过使用 @987654323 调用 main 函数获得的值@ 作为输入。

    所以使用递归函数获取中间值非常繁琐。

    然而,斐波那契计算器可以很容易地内联重写:

    function fibonacci( n ) {
      let a = 1, b = 0, temp;
    
      while( n >= 0 ) {
        temp = a;
        a = a + b;
        b = temp;
        n--;
      }
      return b;
    }
    

    从这里添加经过时间的检查非常容易,并且以我们可以在中间暂停的方式重写它非常简单:

    async function fibonacci( n ) {
      let a = 1, b = 0, temp;
    
      while( n >= 0 ) {
        temp = a;
        a = a + b;
        b = temp;
        n--;
        if( n % batch_size === 0 ) { // we completed one batch
          current_value = b; // let the outside scripts know where we are
          await nextTask(); // let the event-loop loop.
        }
      }
      return b;
    }
    

    要在中间暂停函数,async/await 语法非常方便,因为它允许我们编写线性代码,而不是使用多个复杂的递归回调。
    让事件循环循环的最好方法是 as demonstrated in this answer,使用 MessageChannel 作为下一个任务调度程序。

    现在,您可以让您喜欢的调度方法在这些暂停之间进入,并向主端口发送消息,或侦听来自主线程的更新。


    但是内联您的函数也大大提高了性能,您可以在不到几毫秒的时间内计算出完整的序列,直到 Infinity...(fibonacci( 1476 ) 确实返回 Infinity)。

    所以斐波那契不是证明这个问题的好候选,让我们来计算 π。

    我从this answer借用了一个计算PI的函数,不判断它是否执行,只是为了演示如何让Worker线程暂停一个长时间运行的函数。

    // Main thread code
    const log = document.getElementById( "log" );
    const url = generateWorkerURL();
    const worker = new Worker( url );
    
    worker.onmessage = ({data}) => {
      const [ PI, iterations ] = data;
      log.textContent = `π = ${ PI }
    after ${ iterations } iterations.`
    };
    
    function generateWorkerURL() {
     const script = document.querySelector( "[type='worker-script']" );
     const blob = new Blob( [ script.textContent ], { type: "text/javascript" } );
     return URL.createObjectURL( blob );
    }
    <script type="worker-script">
    // The worker script
    // Will get loaded dynamically in this snippet
    
    // first some helper functions / monkey-patches
    if( !self.requestAnimationFrame ) {
     self.requestAnimationFrame = (cb) => 
       setTimeout( cb, 16 );  
    }
    function postTask( cb ) {
     const channel = postTask.channel;
     channel.port2.addEventListener( "message", () => cb(), { once: true } );
     channel.port1.postMessage( "" );
    }
    (postTask.channel = new MessageChannel()).port2.start();
    function nextTask() {
     return new Promise( (res) => postTask( res ) );
    }
    
    // Now the actual code
    
    // The actual processing
    // borrowed from https://stackoverflow.com/a/50282537/3702797
    // [addition]: made async so it can wait easily for next event loop
    async function calculatePI( iterations = 10000 ) {
    
      let pi = 0;
      let iterator = sequence();
      let i = 0;
      
      // [addition]: start a new interval task
      // which will report to main the current values
      // using an rAF loop as it's the best to render on screen
      requestAnimationFrame( function reportToMain() {
        postMessage( [ pi, i ] );
        requestAnimationFrame( reportToMain );
      } );
    
      // [addition]: define a batch_size
      const batch_size = 10000;
    
      for( ; i < iterations; i++ ){
        pi += 4 /  iterator.next().value;
        pi -= 4 / iterator.next().value;
        // [addition]: In case we completed one batch,
        // we'll wait the next event loop iteration
        // to let the interval callback fire.
        if( i % batch_size === 0 ) {
          await nextTask();
        }
      }
    
      function* sequence() {
        let i = 1;
        while( true ){
          yield i;
          i += 2;
        }
      }
    }
    
    // Start the *big* job...
    calculatePI( Infinity );
    </script>
    <pre id="log"></pre>

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2015-05-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-01-14
      • 2017-03-08
      • 2015-11-22
      相关资源
      最近更新 更多