Sharedarraybuffer mechanics

Where can I read up on the mechanics of SharedArrayBuffer ?

In particular, suppose this.

rust/wasm32/WorkerA : creates a sharedArrayBuffer, shares it with rust/wam32/WorkerB

Now:

rust/wasm32/workerA writes to the SharedArrayBuffer
what do I need to do to ensure that rust/wasm32/workerB sees the updated data ?

On x86, I'd expect "instantaneous"; but from googling, on Chrome/wasm32, I need workerA to postMessage to workerB ?

I'm completely confused on this. Is the mechanics of this documented anywhere ?

Context: workerA is a 'worker' thread computing meshes, workerB is actually an iframe talking to webgl2; I'm looking for the most latency way to send data from workerA to workerB

I wrote the multi-threading/multi-threading code for Wasmer's WASIX in the browser and it's essentially instantaneous.

The bit about postMessage() is probably referring to how you send the SharedArrayBuffer from one worker to another.

The reference I've mainly been relying on is MDN.

Do you use a primitive of this form:

  • workerA does a bunch of writes; worker A does X

  • workerB does Y; worker B does a bunch of reads

  • The system guarantees that all the A/writes before X are visible to the B/reads after Y.

If you are not using postMessage for X/Y; what synch primitive are you using ?

Our situation is a bit complicated because WASIX implements most of an operating system that runs WebAssembly code, including threads and processes.

The WASIX runtime (a wasm32-unknown-unknown program) uses a SharedArrayBuffer so that all workers and the main thread have access to the same address space. There's no need for any extra synchronisation between the workers because we use the normal Rust synchronisation primitives that are available to a wasm32-unknown-unknown build of std (std::sync::atomic::*, std::sync::Mutex, and so on). Passing JavaScript objects between workers (e.g. a js_sys:: WebAssembly::Instance) gets tricky because you need to explicitly pass it via postMessage().

Regarding the happens-before/happens-after relationship, it's the same as normal Rust. You can use normal loads and stores and they're seen whenever (yay, race conditions) or you can use atomics and the updates have a well defined ordering.

Each process gets its own address space (SharedArrayBuffer) that all its threads have access to. If they try to observe reads/writes from multiple threads without using the proper WebAssembly atomic operations then that's a race condition and their problem. Same as if it was x86 code.

Here is an article that discusses using atomics and SharedArrayBuffer from JavaScript. It might be able to answer your questions better than me.

https://blogtitle.github.io/using-javascript-sharedarraybuffers-and-atomics/

Are you aware it is possible for:

workerA:

do a bunch of normal (non-atomic) writes
postMessge to workerB

workerB:

wait for msg
do a bunch of normal (non-atomic) reads

===

and the system guarantees that all the writes of A happens before the reads of B

===

This is what I'm trying to get at -- whether anything besides postMessage/onMessage provides these guarantees.

How exactly are you doing these non-atomic operations?

The only way one worker can communicate with or observe the effects of another worker is via either postMessage() and modifying a shared part of the WebAssembly instance's linear memory (i.e. via static variables).

Rust forces you to make sure all variables inside a static are Sync, which means any accesses need to go through some sort of synchronisation mechanism (e.g. atomics or a mutex), which gives us that well-defined happens-before/happens-after relationship.

If you are trying to observe the effects of two workers without using a synchronisation mechanism, that's UB according to Rust's threading model and SharesArrayBuffer is irrelevant.

Given a SharedArrayBufer, we can construct a U8IntArray from it. Then, we can write to / copy fro the U8IntArray via Uint8Array in js_sys - Rust

This might be the source of our confusion. I am using a SharedArrayBuffer that is NOT part of the wasm linear memory.

I don't see the relevance here.

Given this is SharedArrayBuffer outside the wasm linear memory; I don't believe this is UB. I believe this is at worst a bunch of torn writes/reads + out of order writes/reads.

I almost get the following impression:

  1. You have a mental model where there is 1 SharedArrayBuffer and it is serving as the wasm linear memory for multiple webworkers.

  2. I have a mental model where each webworker has PRIVATE, SOLO wasm linear memory, and they merely have a SharedArrayBuffer (outside of wasm linear memory) that both are using.

Yep, that's exactly it. My responses are for if you use a single SharedArrayBuffer as the linear memory for multiple WebAssembly instances running on different workers. Hence the talk of UB and synchronisation mechanisms.

I think this article from Lin Clark answers the question you're asking:

Basically, if you want to avoid data races then you should use operations from the Atomics namespace when using the same SharedArrayBuffer from multiple workers.

According to the SharedArrayBuffer MDN page, regular writes will be seen "eventually". Which is essentially useless as a guarantee.

the shared data block referenced by the two SharedArrayBuffer objects is the same data block, and a side effect to the block in one agent will eventually become visible in the other agent.

Shared memory can be created and updated simultaneously in workers or the main thread. Depending on the system (the CPU, the OS, the Browser) it can take a while until the change is propagated to all contexts. To synchronize, atomic operations are needed.

This is a very reasonable take.

However, are you aware of javascript - Does `postMessage` or yielding to the event loop or similar sync shared memory? - Stack Overflow

In particular, this section:

To which he replied:

...ensuring (amongst other things) that CPU L1d caches are up-to-date, etc.?

Yes, that was the intent of that language. The writes to the memory should happen-before the postMessage and the receive-message should happen-before the reads.

... That's also a "synchronization edge"?

Yes, same argument. The writes happen-before the wake, and the wakeup of the wait happens-before the reads.

All of this is by intent so as to allow data to be written and read with cheap unsynchronized writes and reads, and then for the (comparatively expensive) synchronization to ensure proper observability.

There appears to be something going on with:

you can do arbitrary (non atomic) writes/reads, and the act of postmessage/onmessage causes a synchronization barrier.

1 Like