如何允许Web Worker在仍执行计算的同时接收新数据?

时间:2019-02-01 11:04:15

标签: javascript sorting interrupt web-worker worker

我想使用Web Workers对数组进行排序。但是,当工作程序仍在执行排序功能时,此数组可能会随着时间的推移接收新值。

所以我的问题是,如何在接收到新项目之后如何“停止”工作程序上的排序计算,以便它可以对该数组执行带有该项目的排序,同时仍然保持已经进行的排序? / p>

示例:

let worker = new Worker('worker.js');
let list = [10,1,5,2,14,3];
worker.postMessage({ list });
setInterval(() => worker.postMessage({ num: SOME_RANDOM_NUM, list }), 100);

worker.onmessage = event => {
  list = event.data.list;
}

所以可以说,我已经超过50岁,在此之前,工人在分类方面取得了一些进展,现在我有了类似的东西: [1, 2, 3, 10, 5, 14, 50]。这意味着排序在索引3处停止。因此,我将此new数组传递回了工作程序,以便它可以继续从位置3进行排序。

由于无法暂停/恢复网络工作者,我该怎么做?

4 个答案:

答案 0 :(得分:5)

即使Worker工作于您主页上的其他线程之外,因此可以连续运行而不会阻塞UI,但它仍在单个线程上运行。

这意味着在您的排序算法完成之前,Worker将延迟消息事件处理程序的执行;它和主线程一样被阻塞。

即使您从该工作人员内部使用其他工作人员,问题也将相同。

唯一的解决方案是使用一种generator function作为排序器,并不时产生它,以便事件得以执行。

但是这样做会大大减慢您的排序算法。

为了更好,您可以尝试挂接到每个事件循环,这要归功于MessageChannel对象:您在一个端口中交谈,并在下一个事件循环中接收消息。如果您再次与另一个端口通信,则您对每个事件循环都有自己的钩子。

现在,最好的方法是在所有这些Event循环中运行一个好的批处理,但是对于演示,我将仅调用生成器函数的一个实例(我是从this Q/A借来的)

const worker = new Worker(getWorkerURL());
worker.onmessage = draw;

onclick = e =>     worker.postMessage(0x0000FF/0xFFFFFF); // add a red pixel

// every frame we request the current state from Worker
function requestFrame() {
  worker.postMessage('gimme a frame');
  requestAnimationFrame(requestFrame);
}
requestFrame();

// drawing part
const ctx = canvas.getContext('2d');
const img = ctx.createImageData(50, 50);
const data = new Uint32Array(img.data.buffer);
ctx.imageSmoothingEnabled = false;

function draw(evt) {
  // converts 0&1 to black and white pixels
  const list = evt.data;
  list.forEach((bool, i) =>
    data[i] = (bool * 0xFFFFFF) + 0xFF000000
  );
  ctx.setTransform(1,0,0,1,0,0);
  ctx.clearRect(0,0,canvas.width,canvas.height);
  ctx.putImageData(img,0,0);
  // draw bigger
  ctx.scale(5,5);
  ctx.drawImage(canvas, 0,0);
}

function getWorkerURL() {
  const script = document.querySelector('[type="worker-script"]');
  const blob = new Blob([script.textContent]);
  return URL.createObjectURL(blob);
}
body{
  background: ivory;
}
<script type="worker-script">
// our list
const list = Array.from({length: 2500}).map(_=>+(Math.random()>.5));
// our sorter generator
let sorter = bubbleSort(list);
let done = false;
/* inner messaging channel */
const msg_channel = new MessageChannel();
// Hook to every Event loop
msg_channel.port2.onmessage = e => {
  // procede next step in sorting algo
  // could be a few thousands in a loop
  const state = sorter.next();
  // while running
  if(!state.done) {
    msg_channel.port1.postMessage('');
    done = false;
  }
  else {
    done = true;
  }
}
msg_channel.port1.postMessage("");

/* outer messaging channel (from main) */
self.onmessage = e => {
  if(e.data === "gimme a frame") {
    self.postMessage(list);
  }
  else {
    list.push(e.data);
    if(done) { // restart the sorter
      sorter = bubbleSort(list);
      msg_channel.port1.postMessage('');
    }
  }
};

function* bubbleSort(a) { // * is magic
  var swapped;
  do {
    swapped = false;
    for (var i = 0; i < a.length - 1; i++) {
      if (a[i] > a[i + 1]) {
        var temp = a[i];
        a[i] = a[i + 1];
        a[i + 1] = temp;
        swapped = true;
        yield swapped; // pause here
      }
    }
  } while (swapped);
}
</script>
<pre> click to add red pixels</pre>
<canvas id="canvas" width="250" height="250"></canvas>

答案 1 :(得分:0)

您可以通过一些技巧来实现–在setTimeout函数中断的帮助下。例如,没有附加线程不可能并行执行2个函数,但是有了setTimeout函数中断技巧,我们可以像下面这样进行操作:

并行执行功能的示例

var count_0 = 0,
    count_1 = 0;

function func_0()
{
    if(count_0 < 3)
        setTimeout(func_0, 0);//the same: setTimeout(func_0);

    console.log('count_0 = '+count_0);
    count_0++
}

function func_1()
{
    if(count_1 < 3)
        setTimeout(func_1, 0);

    console.log('count_1 = '+count_1)
    count_1++
}

func_0();
func_1();

您将获得以下输出:

count_0 = 0
count_1 = 0
count_0 = 1
count_1 = 1
count_0 = 2
count_1 = 2
count_0 = 3
count_1 = 3

为什么有可能?因为setTimeout函数需要一些时间才能执行。这段时间足以执行下面的代码中的某些部分。

为您提供解决方案

在这种情况下,您必须编写自己的数组排序函数(或者也可以使用我的以下函数),因为我们无法中断本机sort函数。在此您自己的函数中,您必须使用此setTimeout函数中断技巧。然后,您会收到message事件通知。

在下面的示例中,我在数组的一半长度处插入了中断,并且可以根据需要进行更改。

自定义排序功能中断的示例

var numbers = [4, 2, 1, 3, 5];

// this is my bubble sort function with interruption
/**
 * Sorting an array. You will get the same, but sorted array.
 * @param {array[]} arr – array to sort
 * @param {number} dir – if dir = -1 you will get an array like [5,4,3,2,1]
 *                 and if dir = 1 in opposite direction like [1,2,3,4,5]
 * @param {number} passCount – it is used only for setTimeout interrupting trick.
 */
function sortNumbersWithInterruption(arr, dir, passCount)
{
    var passes = passCount || arr.length,
        halfOfArrayLength = (arr.length / 2) | 0; // for ex. 2.5 | 0 = 2

    // Why we need while loop: some values are on
    // the end of array and we have to change their
    // positions until they move to the first place of array.
    while(passes--)
    {
        if(!passCount && passes == halfOfArrayLength)
        {
            // if you want you can also not write the following line for full break of sorting
            setTimeout(function(){sortNumbersWithInterruption(arr, dir, passes)}, 0);
            /*
                You can do here all what you want. Place 1
            */
            break
        }

        for(var i = 0; i < arr.length - 1; i++)
        {
            var a = arr[i],
                b = arr[i+1];

            if((a - b) * dir > 0)
            {
                arr[i] = b;
                arr[i+1] = a;
            }
        }

        console.log('array is: ' + arr.join());
    }

    if(passCount)
        console.log('END sring is: ' + arr.join());
}

sortNumbersWithInterruption(numbers, -1); //without passCount parameter
/*
    You can do here all what you want. Place 2
*/
console.log('The execution is here now!');

您将获得以下输出:

数组是:4,2,3,5,1
数组是:4,3,5,2,1
现在是处决了!
数组是:4,5,3,2,1
数组是:5,4,3,2,1
END评分为:5,4,3,2,1

答案 2 :(得分:0)

您可以使用插入排序(种类)来执行此操作。 这是想法:

  1. 以内部空数组开始您的工作程序(显然,空数组已排序)

  2. 您的工作人员仅接收元素,而不接收整个数组

  3. 您的工作人员将接收到的所有元素正确插入到数组中

  4. 每隔n秒,如果上一个事件之后它已更改,则工作程序将使用当前数组引发一条消息。 (如果愿意,可以在每次插入时发送该数组,但是以某种方式缓冲起来效率更高)

最终,您将获得整个阵列,如果添加了任何项目,您将收到更新后的阵列。

注意:由于数组总是排序的,因此可以使用二进制搜索将其插入正确的位置。这非常有效。

答案 3 :(得分:0)

有两个不错的选择。

选项1:Worker.terminate()

第一个只是杀死您现有的网络工作者并开始一个新的。为此,您可以使用Worker.terminate()

terminate()接口的Worker方法立即终止Worker。这不会为工人提供完成作业的机会;它只是立即停止。

此方法的唯一缺点是:

  • 您失去了所有工人状态。如果您必须将大量数据复制到请求中,则必须重新做一遍。
  • 它涉及线程的创建和销毁,这并不像大多数人想象的那样慢,但是如果您终止Web工人很多,则可能会引起问题。

如果这两个都不是问题,那可能是最简单的选择。

就我而言,我有很多状态。我的工作人员正在渲染图像的一部分,当用户平移到其他区域时,我希望它停止正在执行的操作并开始渲染新区域。但是渲染图像所需的数据非常庞大。

在您的情况下,您拥有不想使用的列表状态(可能很大)。

选项2:屈服

第二种选择基本上是进行协作式多任务处理。您可以像往常一样运行计算,但是有时会不停地(屈服)并说“我应该停止吗?”,就像这样(这是一些无意义的计算,不是排序)。

let requestId = 0;

onmessage = event => {
  ++requestId;
  sortAndSendData(requestId, event.data);
}

function sortAndSendData(thisRequestId, data) {
  let isSorted = false;
  let total = 0;

  while (data !== 0) {
    // Do a little bit of computation.
    total += data;
    --data;

    // Check if we are still the current request ID.
    if (thisRequestId !== requestId) {
      // Data was changed. Cancel this sort.
      return;
    }
  }

  postMessage(total);
}

但这不会起作用,因为sortAndSendData()运行完毕并阻止了Web Worker的事件循环。我们需要某种方法在thisRequestId !== requestId之前产生。不幸的是Javascript并没有一个yield方法。它确实有async / await,所以我们可以尝试一下:

let requestId = 0;

onmessage = event => {
  console.log("Got event", event);
  ++requestId;
  sortAndSendData(requestId, event.data);
}

async function sortAndSendData(thisRequestId, data) {
  let isSorted = false;
  let total = 0;

  while (data !== 0) {
    // Do a little bit of computation.
    total += data;
    --data;

    await Promise.resolve();

    // Check if we are still the current request ID.
    if (thisRequestId !== requestId) {
      console.log("Cancelled!");
      // Data was changed. Cancel this sort.
      return;
    }
  }

  postMessage(total);
}

不幸的是,它不起作用。我认为是因为async / await使用“微任务”急切地执行了一些事情,如果可能的话,它们会在挂起的“宏任务”(我们的网络工作者消息)之前执行。

我们需要强制await成为宏任务,您可以使用setTimeout(0)进行操作:

let requestId = 0;

onmessage = event => {
  console.log("Got event", event);
  ++requestId;
  sortAndSendData(requestId, event.data);
}

function yieldToMacrotasks() {
  return new Promise((resolve) => setTimeout(resolve));
}

async function sortAndSendData(thisRequestId, data) {
  let isSorted = false;
  let total = 0;

  while (data !== 0) {
    // Do a little bit of computation.
    total += data;
    --data;

    await yieldToMacrotasks();

    // Check if we are still the current request ID.
    if (thisRequestId !== requestId) {
      console.log("Cancelled!");
      // Data was changed. Cancel this sort.
      return;
    }
  }

  postMessage(total);
}

这有效!但是,它非常慢。 await yieldToMacrotasks()在装有Chrome的计算机上大约需要4毫秒!这是因为浏览器在setTimeout(0)上设置的最小超时时间约为1或4 ms(实际的最小时间似乎很复杂)。

幸运的是,另一个用户将我指向a quicker way。基本上在另一个MessageChannel上发送一条消息也会产生事件循环,但不受setTimeout(0)这样的最小延迟的影响。此代码有效,每个循环仅需约0.04毫秒,就可以了。

let currentTask = {
  cancelled: false,
}

onmessage = event => {
  currentTask.cancelled = true;
  currentTask = {
    cancelled: false,
  };
  performComputation(currentTask, event.data);
}

async function performComputation(task, data) {
  let total = 0;

  let promiseResolver;

  const channel = new MessageChannel();
  channel.port2.onmessage = event => {
    promiseResolver();
  };

  while (data !== 0) {
    // Do a little bit of computation.
    total += data;
    --data;

    // Yield to the event loop.
    const promise = new Promise(resolve => {
      promiseResolver = resolve;
    });
    channel.port1.postMessage(null);
    await promise;

    // Check if this task has been superceded by another one.
    if (task.cancelled) {
      return;
    }
  }

  // Return the result.
  postMessage(total);
}

我对此并不完全满意-它依赖于以FIFO顺序处理的postMessage()事件,我怀疑是否可以保证。我怀疑即使事实并非如此,您也可以重写代码以使其正常工作。