Javascript Synchronize SharedArrayBuffer to main thread

Viewed 1870

I have a problem synchronizing a SharedArrayBuffer to the main thread.

Here is the scenario:

I got two worker which handle different aspects of my program. The first worker is responsible for object interaction, the second for calculating visibilities etc and the main thread will do the visualization.

At first the first Worker creates a SharedArrayBuffer with the following layout:

new SharedArrayBuffer(112);
[   
    Lock:      4 Byte
    MetaInfo:  4 Byte
    Location: 12 Byte
    Scale:    12 Byte
    Rotation: 16 Byte
    Matrix:   64 Byte
]

He then sends the SAB to the main thread and second Worker and stores the location scale and rotation attributes in the Buffer. Every time he updates the fields he locks the SAB, updates the values and sets the first bit of the MetaInfo fields (transform flag) to true.

The second Worker will compute the matrix from the given location scale and rotation fields if the transform flag is set and save it in the Matrix fields. Afterward the second bit of the MetaInfo fields (matrix flag) will be set to true.

The main thread now needs to read the final matrix if the matrix flag is set.

Here comes the problem: On the workers it is possible to lock the buffer using the Atomics.wait method on the Lock fields. But the main thread lacks such features resulting in stuttering and "hopping". Is there a consistent way to prevent the other worker from writing into the SAB during the reading process?

Here is the code of my SharedArrayBuffer wrapper:

class SharedObject {
    SharedBuffer: SharedArrayBuffer; // the shared array buffer
    Lock: Int32Array;  // view for lockíng the buffer
    MetaInfo: Int32Array; // view for meta info
    Location: Float32Array;

    constructor(buffer) {
        // if valid buffer is passed assign it to this object
        if (buffer !== undefined && buffer instanceof SharedArrayBuffer && buffer.byteLength == 112) {
            this.SharedBuffer = buffer;
        } else {
            // create new shared array buffer
            this.SharedBuffer = new SharedArrayBuffer(112);
        }

        this.Lock = new Int32Array(this.SharedBuffer, 0, 4);
        this.MetaInfo = new Int32Array(this.SharedBuffer, 4, 8);

        [ ... init the rest of the views ... ]

        // init the lock element
        if (buffer === undefined) {
            Atomics.store(this.Lock, 0, 1);
        }

    }

    lock() {
        Atomics.wait(this.Lock, 0, 0);
        Atomics.store(this.Lock, 0, 0);
        return true;
    }

    free() {
        if (Atomics.wake(this.Lock, 0, 1) == 0) {
            Atomics.store(this.Lock, 0, 1);
        }
        return true;
    }

    setFlag(flag) {
        this.MetaInfo[0] = this.MetaInfo[0] | flag;
    }
    isFlagSet(flag) {
        return (this.MetaInfo[0] & flag) > 0;
    }
    resetFlag(flag) {
        this.MetaInfo[0] = this.MetaInfo[0] - (this.MetaInfo[0] & flag);
    }
}

Note the lock and free method are not use able in main thread since:

Note: This operation only works with a shared Int32Array and is not allowed on the main thread.

https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Atomics/wait

Is this setup even possible to have multiple independent fields in one SharedArrayBuffer or should I consider using multiple SharedArrayBuffer for each application.

2 Answers

Don't think the problem is still relevant. However, if someone faces the same issue, here are my thoughts:

  1. Do not implement lock mechanism yourself, use library like https://github.com/lars-t-hansen/js-lock-and-condition/blob/master/lock.js

    Here is a problem in lock function:

        Atomics.wait(this.Lock, 0, 0); // <-- 2 threads can check 'lock' flag one by one 
                                       //     and pass to the next line
        Atomics.store(this.Lock, 0, 0); // <-- then they both set 0 as a 'lock' flag
                                        //     and move further
        return true;
    }

So, you'll have a race condition.

Also, checkout this demo app I have written to test SharedArrayBuffer https://github.com/vrudkovskiy/textediting-test-js/blob/master/packages/client/src/utils/SharingLock.ts

Update.

Atomics class provides atomic operations only on a single buffer element. But, as I understood, you needed to lock access to the whole buffer while some thread reads/writes from/to it.

So, for example, if 2 threads write data to the same part of memory you can have following situation:
Thread 1 writes: 11111111
Thread 2 writes: 22222222
Result can be: 11221122 - it would be some random result

And, unfortunately, there is no atomic semaphore in Atomics and it should be implemented using a combination of Atomics.compareExchange and Atomics.wait calls. It is really not easy, cause there are lot of cases you should cover, that's why I say not to do it yourself. You can find better explanation here

  1. In this problem it is not required to block main thread, but instead main thread should block others to access shared memory while main thread is reading it. TryLock function can be used https://github.com/lars-t-hansen/js-lock-and-condition/blob/master/lock.js#L126

In this case main thread does several tries to lock shared memory, let's say every frame(rendering is not freezed), and once it succeed - it renders data.

Update.

Main thread doesn't access data it only can try to block data one more time. Of course there would be a performance drawback.

  1. Another technique is to not use Atomics but posting messages instead. For example:

    • worker calculated some data
    • it sends a notification to main thread about it
    • worker does not use that part of memory till main thread allows it by notifying back through postMessage function
  2. And the last point. An author of the lock library above has written a proposal to a standard for async locking https://github.com/tc39/proposal-atomics-wait-async It contains a polyfill which uses 3rd worker just to lock data for main thread, in other words main thread delegates locking a resource to separate worker which notifies parent thread about successful locking. Polyfill contains an obvious performance drawback, but native implementation should be much faster

Update.

You can try to combine tryLock in main thread with a frame drawing synchronization in worker thread(somehow predict when main thread draws data and don't touch it at that time), sounds like a workaround and I'm not sure if it possible even :)

Or, maybe it would be ok to not block access to buffer at all(just draw results like 11221122), so just use Atomics to read/change single element. Or lock access to only some small parts of buffer.

In any case there is no ideal solution with multithreading, you always choose between performance and data consistency.

Related