24

Is there some way to synchronously wait for or check for a new message in a web-worker?

I have a large complicated body of code (compiled LLVM from emscripten) that I cannot refactor around callbacks.

I need to make sure that code after a certain line doesn't execute until I receive and handle a message from the UI-thread. If I block with a while-loop, the event-loop never runs so I can't receive messages.

3
  • 1
    There would be a solution to your problem if ECMAScript was a language as rich as Scheme with its call-with-continuation. This would allow you to catch the current continuation and call it again in your onmessage handler. Commented Jan 5, 2015 at 11:55
  • 1
    It is now possible with the Atomics API or by abusing a service worker glitch.com/edit/#!/sleep-sw?path=worker.js%3A28%3A71 Commented Jul 18, 2021 at 10:42
  • 1
    @Stefnotch the SW hack is quite clever, but to really do what OP wanted you'd need to pass the data through the response. They can't access the Worker's message-queue because their burnCpu never comes back to the event-loop (no setTimeout). However from your SW you could wait that the main thread posts a message to the SW (instead of waiting the time param), and pass that as the response to the sync XHR. Though you'd be limited to Strings, ArrayBuffers and Blobs, and would be copying the two last ones instead of just linking to them... but might be a good answer here anyway. Commented Aug 27, 2021 at 1:19

5 Answers 5

10
+100

This is an issue that I also ran into while working with Pyodide. I wanted to 'synchronously' call a function from the main thread.

One solution involves the Atomics and SharedArrayBuffer APIs. From the perspective of the web worker, this looks like the following

  1. postMessage the main thread
  2. freeze ourselves with Atomics.wait
  3. get unfrozen by the main thread
  4. read the result from a SharedArrayBuffer. We can't receive the result as a postMessage, because there isn't a synchronous way of asking "did I get a message".

Of course this requires a fair amount of extra code to handle all the serialization, data passing and more.

The main limitation is that to use those APIs, one needs to have the COOP/COEP headers set. The other bit to keep in mind is that this only works on recent browsers such as Safari 15.2 (released in December 2021).

There is also an alternative solution with synchronous XHR and a service worker, but I haven't investigated that option.

Sign up to request clarification or add additional context in comments.

5 Comments

You could probably use the SharedArrayBuffer itself to pass some data from the main thread. See github.com/GoogleChromeLabs/buffer-backed-object for some ideas on how to serialize most objects.
This is not a good answer. The answer by Pasha Bolokhov stackoverflow.com/a/68945416/857427 is in fact the correct one. I don't know why it hasn't received enough attention.
@arashka because they don't answer the question as it's being asked. Maybe it solves your issue, but it won't solve the one presented in the original post here.
@Kaiido Please read my comment on your other comment on the answer by Pasha Bolokhov (stackoverflow.com/a/68945416/857427). You are misinterpreting the question. As this user asks, he wants to make sure a part of code is executed only after another (I need to make sure that code after a certain line doesn't execute until I receive and handle a message from the UI-thread). This doesn't refer to the whole runtime, this is about a code block, as we usually consider when we speak about synchronous execution model. So async/await means exactly synchronous in this context.
No it does refer to the runtime since otherwise they would have been able to refactor it to use callbacks. They want to "synchronously wait for or check for a new message in a web-worker". Using async is obviously not the solution, it's in the name.
3

This is how I've done this. It's really sorrowful to think that Web Workers haven't received the await message functionality, which would make the whole inter-thread communication concept a lot closer to Go language for example. But no, they ain't did it, so have to invent stuff in order to keep threads synchronised

 let resolver: any = undefined; let message: any = undefined; const messagePromise = new Promise((resolve) => resolver = resolve); worker.onmessage = async ({ data: payload }: { data: any }) => { message = payload; worker.onmessage = undefined; resolver?.(); } await messagePromise; 

Now, this is Typescript. Just remove the types to get Javascript. Note that there is no way to capture a message if it had been sent before we started listening for messages

12 Comments

This is not really what OP asked for. Their case was that they can't break their code to run on multiple tasks, i.e it can't let the event-loop actually loop and process its queued messages. If you wish a Promise based messaging system between your UI context and the Worker one, then have a look at MessageChannels which will allow your code to create one-off channels on which you'll be sure to be the only one to listen to. Here is an example: stackoverflow.com/questions/62076325/…
Message channels are not blocking either. There's no built in way to say "I want to wait until I get a message"
No but it helps doing what you are trying to do: promisify the communication
No, I'm trying to synchronize threads, like it would happen in Go
But the code in your answer is just creating a promisified communication system, except that it won't handle well multiplexing.
|
2

A crude hack to do this could be to use a synchronous API, such as the FileSystem API, and use a temporary file as a synchronising mechanism.

In your example, the emscripten code could issue a synchronous read on a file, while the UI thread writes to the same file, effectively unblocking the worker.

Comments

1

No, unfortunately. There was some discussion of adding a way to block on a message from the parent page, but it went nowhere. I'll see if I can dig up the relevant mailing list thread.

Comments

1

Can you break the sequence as follow without refactoring?

wrk.onmessage = function(e) { if (e.data.step === 'initial input') { doInitialWork(e.data.inputs) } else if (e.data.step === 'ui input') { doTheRest(e.data.uiData) } } 

The onmessage event will be blocked until the initial step execution stack is complete and the critical ui bits will only launch when and if the info is available.

1 Comment

This isn't truly blocking. You still end up with two function calls. For example, a blocking web worker would be able to implement a function which takes a forEach style function and converts it into an iterator automatically.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.