如何让 Web Worker 在计算的同时接收新数据?
How to allow Web Workers to receive new data while it still performing computation?
我想使用 Web Workers 对数组进行排序。但是随着时间的推移,这个数组可能会收到新的值,而工作人员仍在执行排序功能。
所以我的问题是,我怎样才能 "stop" 在收到新项目后对 worker 进行排序计算,以便它可以对包含该项目的数组执行排序,同时仍然保持原来的排序已经做了?
示例:
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
.
继续排序
我怎样才能做到这一点,因为没有办法 pause/resume 网络工作者?
即使 Worker 在您的主页以外的其他线程上工作,因此可以 运行 连续地不阻塞 UI,它仍然 运行 在单线程。
这意味着在你的排序算法完成之前,Worker 将延迟消息事件处理程序的执行;它和主线程一样被阻塞。
即使你使用了这个worker内部的另一个Worker,问题也是一样的。
唯一的解决方案是使用一种 generator function 作为排序器,并时不时地产生它以便事件可以得到执行。
但是这样做会大大降低排序算法的速度。
为了让它变得更好,您可以尝试挂钩每个事件循环,这要归功于 MessageChannel 对象:您在一个端口中通话并在下一个事件循环中接收消息。如果你再次与另一个端口对话,那么你就有了自己的每个事件循环的钩子。
现在,最好的办法是 运行 在每个事件循环中进行良好的批处理,但为了演示,我将只调用生成器函数的一个实例(我从 )
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>
请注意,同样可以使用异步函数来实现,这在某些情况下可能更实用:
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 done = false;
/* 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
bubbleSort(list);
}
}
};
async function bubbleSort(a) { // async is magic
var swapped;
do {
swapped = false;
for (var i = 0; i < a.length - 1; i++) {
if (a[i] > a[i + 1]) {
const temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
swapped = true;
}
if( i % 50 === 0 ) { // by batches of 50?
await waitNextTask(); // pause here
}
}
} while (swapped);
done = true;
}
function waitNextTask() {
return new Promise( (resolve) => {
const channel = waitNextTask.channel ||= new MessageChannel();
channel.port1.addEventListener("message", (evt) => resolve(), { once: true });
channel.port2.postMessage("");
channel.port1.start();
});
}
bubbleSort(list);
</script>
<pre> click to add red pixels</pre>
<canvas id="canvas" width="250" height="250"></canvas>
你可以用一些技巧来做到这一点——借助 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!');
你会得到这个输出:
array is: 4,2,3,5,1
array is: 4,3,5,2,1
The execution is here now!
array is: 4,5,3,2,1
array is: 5,4,3,2,1
END sring is: 5,4,3,2,1
你可以用插入排序(某种程度上)来做到这一点。
这是想法:
用一个内部空数组启动你的worker(空数组显然是排序的)
你的工人只接收元素而不是整个数组
您的工作人员将任何收到的元素正确插入数组的正确位置
每隔 n 秒,如果当前数组在上次事件后发生更改,工作人员将使用当前数组引发一条消息。 (如果你愿意,你可以在每次插入时发送数组,但以某种方式缓冲更有效)
最终,您将获得整个数组,如果添加了任何项目,您将收到更新后的数组。
注意:因为您的数组总是排序的,所以您可以使用二进制搜索将其插入到正确的位置。这样效率很高。
有两个不错的选择。
选项 1:Worker.terminate()
第一个是杀死现有的 web worker 并启动一个新的。为此,您可以使用 Worker.terminate()
.
The terminate()
method of the Worker
interface immediately terminates the Worker
. This does not offer the worker an opportunity to finish its operations; it is simply stopped at once.
这种方法的唯一缺点是:
- 你失去了所有工人状态。如果您必须为请求将大量数据复制到其中,则必须重新执行所有操作。
- 它涉及线程的创建和销毁,这并不像大多数人想象的那么慢,但是如果您终止 web worker 很多,它可能会导致问题。
如果这些都不是问题,这可能是最简单的选择。
就我而言,我有很多状态。我的工作人员正在渲染图像的一部分,当用户平移到另一个区域时,我希望它停止正在做的事情并开始渲染新区域。但是渲染图像所需的数据非常庞大。
在你的情况下,你有你不想使用的(可能是巨大的)列表的状态。
选项 2:屈服
第二个选项基本上是进行协作式多任务处理。你运行你的计算正常,但你时不时地停下来(屈服)并说"should I stop?",就像这样(这是一些无意义的计算,而不是排序)。
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()
运行s 完成并阻止网络工作者的事件循环。我们需要一些方法在 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
使用 "microtasks" 急切地执行事情,如果可能的话,它会在等待 "macrotasks"(我们的网络工作者消息)之前执行。
我们需要强制我们的 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 毫秒(实际最小值似乎很复杂)。
幸运的是,另一位用户向我指出了 。基本上在另一个 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);
}
我对此并不完全满意 - 它依赖于 postMessage()
以 FIFO 顺序处理的事件,我怀疑这是否能得到保证。我怀疑您可以重写代码以使其正常工作,即使事实并非如此。
我想使用 Web Workers 对数组进行排序。但是随着时间的推移,这个数组可能会收到新的值,而工作人员仍在执行排序功能。
所以我的问题是,我怎样才能 "stop" 在收到新项目后对 worker 进行排序计算,以便它可以对包含该项目的数组执行排序,同时仍然保持原来的排序已经做了?
示例:
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
.
我怎样才能做到这一点,因为没有办法 pause/resume 网络工作者?
即使 Worker 在您的主页以外的其他线程上工作,因此可以 运行 连续地不阻塞 UI,它仍然 运行 在单线程。
这意味着在你的排序算法完成之前,Worker 将延迟消息事件处理程序的执行;它和主线程一样被阻塞。
即使你使用了这个worker内部的另一个Worker,问题也是一样的。
唯一的解决方案是使用一种 generator function 作为排序器,并时不时地产生它以便事件可以得到执行。
但是这样做会大大降低排序算法的速度。
为了让它变得更好,您可以尝试挂钩每个事件循环,这要归功于 MessageChannel 对象:您在一个端口中通话并在下一个事件循环中接收消息。如果你再次与另一个端口对话,那么你就有了自己的每个事件循环的钩子。
现在,最好的办法是 运行 在每个事件循环中进行良好的批处理,但为了演示,我将只调用生成器函数的一个实例(我从
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>
请注意,同样可以使用异步函数来实现,这在某些情况下可能更实用:
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 done = false;
/* 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
bubbleSort(list);
}
}
};
async function bubbleSort(a) { // async is magic
var swapped;
do {
swapped = false;
for (var i = 0; i < a.length - 1; i++) {
if (a[i] > a[i + 1]) {
const temp = a[i];
a[i] = a[i + 1];
a[i + 1] = temp;
swapped = true;
}
if( i % 50 === 0 ) { // by batches of 50?
await waitNextTask(); // pause here
}
}
} while (swapped);
done = true;
}
function waitNextTask() {
return new Promise( (resolve) => {
const channel = waitNextTask.channel ||= new MessageChannel();
channel.port1.addEventListener("message", (evt) => resolve(), { once: true });
channel.port2.postMessage("");
channel.port1.start();
});
}
bubbleSort(list);
</script>
<pre> click to add red pixels</pre>
<canvas id="canvas" width="250" height="250"></canvas>
你可以用一些技巧来做到这一点——借助 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!');
你会得到这个输出:
array is: 4,2,3,5,1
array is: 4,3,5,2,1
The execution is here now!
array is: 4,5,3,2,1
array is: 5,4,3,2,1
END sring is: 5,4,3,2,1
你可以用插入排序(某种程度上)来做到这一点。 这是想法:
用一个内部空数组启动你的worker(空数组显然是排序的)
你的工人只接收元素而不是整个数组
您的工作人员将任何收到的元素正确插入数组的正确位置
每隔 n 秒,如果当前数组在上次事件后发生更改,工作人员将使用当前数组引发一条消息。 (如果你愿意,你可以在每次插入时发送数组,但以某种方式缓冲更有效)
最终,您将获得整个数组,如果添加了任何项目,您将收到更新后的数组。
注意:因为您的数组总是排序的,所以您可以使用二进制搜索将其插入到正确的位置。这样效率很高。
有两个不错的选择。
选项 1:Worker.terminate()
第一个是杀死现有的 web worker 并启动一个新的。为此,您可以使用 Worker.terminate()
.
The
terminate()
method of theWorker
interface immediately terminates theWorker
. This does not offer the worker an opportunity to finish its operations; it is simply stopped at once.
这种方法的唯一缺点是:
- 你失去了所有工人状态。如果您必须为请求将大量数据复制到其中,则必须重新执行所有操作。
- 它涉及线程的创建和销毁,这并不像大多数人想象的那么慢,但是如果您终止 web worker 很多,它可能会导致问题。
如果这些都不是问题,这可能是最简单的选择。
就我而言,我有很多状态。我的工作人员正在渲染图像的一部分,当用户平移到另一个区域时,我希望它停止正在做的事情并开始渲染新区域。但是渲染图像所需的数据非常庞大。
在你的情况下,你有你不想使用的(可能是巨大的)列表的状态。
选项 2:屈服
第二个选项基本上是进行协作式多任务处理。你运行你的计算正常,但你时不时地停下来(屈服)并说"should I stop?",就像这样(这是一些无意义的计算,而不是排序)。
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()
运行s 完成并阻止网络工作者的事件循环。我们需要一些方法在 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
使用 "microtasks" 急切地执行事情,如果可能的话,它会在等待 "macrotasks"(我们的网络工作者消息)之前执行。
我们需要强制我们的 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 毫秒(实际最小值似乎很复杂)。
幸运的是,另一位用户向我指出了 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);
}
我对此并不完全满意 - 它依赖于 postMessage()
以 FIFO 顺序处理的事件,我怀疑这是否能得到保证。我怀疑您可以重写代码以使其正常工作,即使事实并非如此。