How to let a webworker do multiple tasks simultaneously?
Asked Answered
T

1

1

I am trying to let a Web-Worker manage its state, meanwhile serving multiple async requests.

worker.ts file

let a =0; //this is my worker's state

let worker=self as unknown as Worker;

worker.onmessage =(e)=>{
    console.log("Rec msg", e.data);

    if(e.data === "+1"){
        setTimeout(()=>{
            a=a+1;
            worker.postMessage(a);
        },3000);
    }else if(e.data=== "+2"){
        setTimeout(()=>{
            a=a+2;
            worker.postMessage(a);
        },1000)
    }
}

And this is my main file: main.ts

let w =new Worker("./worker.ts", {type: "module"})

let wf =async (op: string)=>{
    w.postMessage(op);
    return new Promise<any>((res,rej)=>{
        w.onmessage=res;
    });
}

(async()=>{
    let f1 = await wf("+1");
    console.log("f1",f1.data);
})();

(async()=>{
    let f2 = await wf("+2");
    console.log("f2",f2.data);
})()

Only f2 is returned , and f1 is lost. I have used timeouts to simulate say some async task done by worker themselves.

How do I receive both f1 and f2?

Thundercloud answered 28/5, 2020 at 23:44 Comment(0)
R
7

Your problem is that you are trying to take an event based API and use it as a Promise based one, but events may fire multiple times, while Promise should resolve only once.

The communication between the Worker and the main thread works by sending and receiving messages, but there is by default no one-to-one relation between these messages. Both ends of the communication (ports) will simply stack incoming messages, and handle them sequentially, when they'll get time.

In your code, the main thread's worker.onmessage handler of f1 has been overwritten by the second call f2 synchronously (one microtask later, but that's still synchronous for our matter).
You could attach your event using the addEventListener method, at least this way it wouldn't be overwritten. But even then, when the first message event will fire on worker, both handlers will think it's there own message that did arrive, while in fact it was the one of f2. so that's not what you need...

What you need is to set up a protocol of communication which would allow both ends to identify each task. You could for instance wrap all your tasks' data with an object containing a .UIID member, be sure both ends wraps their message this way, and then from main thread check that UUID to resolve the appropriate Promise.

But that can become a bit complicated to implement and to use.


My personal favorite way is to create a new MessageChannel per task. If you don't know this API, I invite you to read this answer of mine explaining the basics.

Since we are sure the only one message that will come through this MessageChannel is the response from the Worker to the one task we sent to it, we can await it just like a Promise.

All we have to do, is to make sure that in the Worker thread we respond through the transferred port instead of the global scope.

const url = getWorkerURL();
const worker = new Worker(url)

const workerFunc = (op) => {
  // we create a new MessageChannel
  const channel = new MessageChannel();
  // we transfer one of its ports to the Worker thread
  worker.postMessage(op, [channel.port1]);

  return new Promise((res,rej) => {
    // we listen for a message from the remaining port of our MessageChannel
    channel.port2.onmessage = (evt) => res(evt.data);
  });
}

(async () => {
  const f1 = await workerFunc("+1");
  console.log("f1", f1);
})();

(async () => {
  const f2 = await workerFunc("+2");
  console.log("f2", f2);
})()


// SO only
function getWorkerURL() {
  const elem = document.querySelector( '[type="worker-script"]' );
  const script = elem.textContent;
  const blob = new Blob( [script], { type: "text/javascript" } );
  return URL.createObjectURL( blob );
}
<script type="worker-script">
let a = 0;
const worker = self;

worker.onmessage = (evt) => {
  const port = evt.ports[0]; // this is where we will respond
  if (evt.data === "+1") {
    setTimeout(() => {
      a = a + 1;
      // we respond through the 'port'
      port.postMessage(a);
    }, 3000);
  }
  else if (evt.data === "+2") {
    setTimeout(() => {
      a = a + 2;
      // we respond through the 'port'
      port.postMessage(a);
    }, 1000)
  }
};
</script>
Rarefy answered 29/5, 2020 at 1:49 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.