重复使用网络工作者 vs 关闭/产生新的一次?

van*_*owm 3 javascript web-worker

我使用网络工作者来计算用户单击按钮时元素的位置。每次点击都会产生一个新的工作人员。一旦工作人员完成其任务,它就会终止。

我注意到,有时主进程收到的新生成的工作人员的响应会有延迟(有时是一整秒,当浏览器空闲一段时间时),所以我尝试将工作人员留在内存中并重新使用它们,只是如果所有人都很忙,就会产生新的。这似乎可以解决延迟问题(平均而言,通过这种方式,第一个响应的接收速度也快了 4-6 倍),但现在我想知道,从长远来看,让工作人员留在内存中是一个好主意,特别是因为在这种情况下例如每个消耗 1.5mb RAM?

for(let i = 0; i < 100; i++)
  table.appendChild(document.createElement("div"));

const workerURL = URL.createObjectURL(new Blob(["(" + (() =>
{
  const timeNow = performance.now();
  // worker code
  const pref = {};
  this.onmessage = e => {
    Object.assign(pref, e.data);
    if (pref.status)
      this.postMessage(performance.now() - pref.time);
  }; //set preferences
  (function loop(timestamp = performance.now())
  {
    setTimeout(loop);
    if (!pref.status || timestamp - pref.time < pref.speed)
      return;

    pref.time = timestamp;
    pref.position++;
    this.postMessage(pref); //send new position
  })();
}).toString() + ")()"]));

const animations = (() =>
{
  const workers = new Map();
  const showWorkers = (i=0) =>
  {
    while(workersList.children.length > workers.size)
      workersList.removeChild(workersList.lastChild);

    let busy = 0
    for(let [_worker, pref] of workers)
    {
      const node = workersList.children[i++] || document.createElement("span");
      node.textContent = pref.id;
      node.classList.toggle("active", pref.status);
      node.style.setProperty("--bg", "#" + pref.bg);
      node.style.setProperty("--color", pref.color);

      if (pref.status)
        busy++;

      if (!node.parentNode)
        workersList.appendChild(node);
    }
    stats.textContent = "Workers: " + workers.size + " Busy: " + busy;
  };
  const initPref = pref =>
  {
    const defaultPref = {
        bg: (~~(Math.random() * 16777215)).toString(16).padStart(6, 0),
        speed: ~~(Math.random() * 50 + 10),
      };

    pref = Object.assign({},
            defaultPref, //set default parameters
            pref, //restore parameters from existing worker
            persist.checked ? {} : defaultPref, //reset to default
            {position: -1, status: 1, time: 0});//reset some of the parameters

    pref.color = ["black","white"][~~([0,2,4].map(p=>parseInt(pref.bg.substr(p,2),16)).reduce((r,v,i)=>[.299,.587,.114][i]*v+r,0)<128)]; //luminiacity
    return pref;
  }
  showWorkers();

  let id = 1;
  return {
    start: () =>
    {
      const timeStart = performance.now();
      let worker, pref = {}, prevNode;
      //find idle worker
      for(let [_worker, _pref] of workers)
      {
        if (!_pref.status)
        {
          worker = _worker;
          pref = _pref;
          break;
        }
      }
      pref = initPref(pref);
      if (!worker)
      {
        worker = new Worker(workerURL);
        pref.id = id++
      }
      worker.onmessage = e =>
      {
        if (typeof e.data == "number")
        {
          return response.textContent = ("Worker " + pref.id + " responded in " + (performance.now() - timeStart).toFixed(1) + "ms\n" + response.textContent).split("\n").slice(0, 100).join("\n");
        }

        Object.assign(pref, e.data); //update pref
        if (prevNode)
        {
          prevNode.removeAttribute("style");
          delete prevNode.dataset.id;
        }

        if (pref.position >= table.children.length) //finish worker
        {
          if (reuseWorkers.checked)
          {
            pref.status = 0;
            worker.postMessage(pref); //stop worker
          }
          else
          {
            worker.terminate();
            workers.delete(worker);
          }
          showWorkers(); //update stats
          return;
        }
        const node = table.children[pref.position];
        node.style.setProperty("--bg", "#" + pref.bg);
        node.style.setProperty("--color", pref.color);
        node.dataset.id = pref.id;
        prevNode = node;
      };
      worker.postMessage(pref);
      workers.set(worker, pref);
      showWorkers();
    },
  }
})();

start.onclick = () => animations.start();
Run Code Online (Sandbox Code Playgroud)
#table
{
  display: inline-grid;
  grid-template-columns: repeat(10, 10fr);
}
#table > *
{
  outline: 1px solid black;
  background-color: white;
  width: 1em;
  height: 1em;
}
#table > *::before
{
  content: "";
}
#table > [style]::before
{
  content: attr(data-id);
  width: 125%;
  height: 125%;
  font-size: 0.7em;
  line-height: 1.75em;
  display: inline-flex;
  justify-content: center;
  position: relative;
  background-color: var(--bg);
  color: var(--color);
  border: 1px solid;
  top: -25%;
  left: -25%;
  border-radius: 1em;
}

.nowrap
{
  white-space: nowrap;
  display: flex;
  align-items: flex-start;
}
#workersList
{
  display: inline-flex;
  flex-wrap: wrap;
  align-content: flex-start;
  margin-left: 0.5em;
}
#workersList > *
{
  padding: 0.2em;
  border: 1px solid black;
  margin: -1px 0 0 -1px;
  background-color: white;
  min-width: 1em;
  height: 1em;
  text-align: center;
}
#workersList > .active
{
  background-color: var(--bg);
  color: var(--color);
  border-radius: 100%;
}
#response
{
  white-space: pre;
  max-height: 10em;
  overflow: auto;
  vertical-align: top;
  display: inline-block;
  margin: 0 0.5em;
  padding: 0 0.5em;
  flex-shrink: 0;
}
#stats
{
  margin-left: 1em;
}
Run Code Online (Sandbox Code Playgroud)
<div>
  <button id="start">Start</button>
  <label><input type="checkbox" id="reuseWorkers">Re-use idle workers</label>
  <label><input type="checkbox" id="persist">Re-use color/speed</label>
  <span id="stats"></span>
</div>
<div class="nowrap">
  <div id="table"></div>
  <div id="response"></div>
  <div id="workersList"></div>
</div>
Run Code Online (Sandbox Code Playgroud)

PS 如果它闲置了一段时间,我可能可以添加自我终止。

Kai*_*ido 6

是的,绝对应该重用你的工人。
启动一个新的 Worker 意味着必须生成一个新的进程、一个新的事件循环和一个新的 JS 上下文。正如规范所述,这是一项繁重的操作:

Workers(这些后台脚本在本文中被称为)相对较重,并且不适合大量使用。例如,为四兆像素图像的每个像素启动一名工作人员是不合适的。[...]

一般来说,worker 的寿命应该很长,启动性能成本很高,每个实例的内存成本也很高。

有一些积极的讨论(我参加了),以改进Worker界面以防止网络作者做你所做的事情。目前,一次性 Workers 太诱人了,因为设置多任务 Workers 相对复杂。希望这次讨论能让 Workers 易于重用。

实际上,根据经验,您启动的 Workers 数量永远不应该超过navigator.hardwareConcurrency - 1。如果不这样做,您的 Workers 的并发性将通过任务切换而不是真正的并行性来完成,这会导致显着的性能损失。

关于 Workers 的内存消耗,如果您正确地删除了对每个任务中使用的对象的引用,垃圾收集器通常会完成其工作,即使压力来自另一个线程。正如规范引用所暗示的那样,即使是空的 Worker 也有自己的内存占用,因此启动大量 Worker 实际上会增加更多的压力。