This proposal aims to enable developers to wrap JavaScript values in synchronization primitives to allow sending them across threads & JavaScript contexts.
Stage: Not proposed yet
Authors:
- David Alsh
Champions:
The addition of a Mutex primitive that is considered "transferrable" (like SharedArrayBuffer).
A Mutex captures a JavaScript value and ensures that access to that value can only occur when a lock for it is acquired.
While a lock is held, all additional attempts to access the lock will wait until the lock is released.
This allows for mutable data to be defined in one JavaScript context and interacted with from external JavaScript contexts.
const m = new Mutex({
i: 0,
increment() {
this.i++
}
})
// Acquire a lock, no one else can use the value
const guard = await m.lock()
console.log(guard.value.i)
// Complex values like class instances, functions can be stored
guard.value.increment()
console.log(guard.value.i)
// Release the lock so others can access the value
guard.release()In the example above, a Mutex is created that wraps an object. The value within the Mutex can be obtained via Mutex.lock() and the lock released with Mutex.release()
While a lock is held, all additional attempts to access the lock will wait until the lock is released.
Once a value is captured by a Mutex, all additional references become null (input wanted) ensuring the value can only be accessed via the lock
// main.js
const m = new Mutex({
i: 0,
})
// Spawn a worker thread and transfer the lock by reference
const worker = new Worker('./worker.js')
worker.postMessage(null, [m])
// Poll the lock and inspect the value
setInterval(async () => {
const guard = await m.lock()
console.log(guard.value.i)
guard.release()
}, 100)// worker.js
onmessage = async (m) => {
const guard = await m.lock()
guard.value.i++
guard.release()
}In the example above the Mutex is transferred into the context of a Worker which obtains the value, mutates it, and releases the lock.
The addition of a RwLock primitive that is considered "transferrable". This captures a JavaScript value and ensures that access to
that value can only occur when a read or write lock for it is acquired.
A RwLock can have unlimited reads OR only one write.
This allows for mutable data to be defined in one JavaScript context and interacted with from external JavaScript contexts. This is for use cases where the data is constructed in one phase of an application's life cycle and read from multiple threads later.
// main.js
const rw = new RwLock([])
// Do "work"
const guard = await rw.write()
for (let i = 0; i < 100; i++) guard.value.push(i)
guard.release()
// Spawn workers that will read the contents
for (let i = 0; i < 10; i++) {
const worker = new Worker('./worker.js')
worker.postMessage(null, [rw])
}// worker.js
onmessage = async (m) => {
// The workers can all "read" at the same time
// as unlimited "read" locks can be granted simultaneously
const guard = await m.read()
console.log(guard.value.i)
guard.release()
}This proposal stacks nicely with Symbol.dispose
async function main() {
const m = new Mutex(42)
await using guard = m.lock()
console.log(guard.value)
// guard is automatically unlocked
// at the end of the scope
}Due to the implementations of various JavaScript engines using isolated heaps with independent garbage collectors, it would be best if these synchronization primitives were implemented using reference counted smart pointers that would persist until all references were removed.
This would allow the values to work across multiple session contexts; tabs/windows via BroadcastChannel, in the background via ServiceWorker and in the current session via Worker contexts.
const a = {}
const b = a
const m = new Mutex(a)
console.log(b) // ??let a
const m = new Mutex({})
const b = await m.lock()
a = b
m.release()
console.log(a) // ??