3

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.

6
  • Did you just start this project very recently? I see Chrome just rolled out support for SharedArrayBuffer on Thursday where it's not behind a flag. Commented Aug 1, 2017 at 13:54
  • That is good news, perhaps it resolves the problem sending SharedArrayBuffer via message ports. But sadly i am working inside nwjs currently in chrome 60. Commented Aug 1, 2017 at 14:01
  • Oh, interesting.. I haven't heard of anyone using that in a while, but I will say that Chrome 60 is the first version to have official support for SharedArrayBuffer. Are you required to use nwjs or will you be able to switch easily to using browser-based JavaScript? Commented Aug 1, 2017 at 14:05
  • We are building an app that requires some filesystem interactions. But the reanderer should be able to run entirely in browser if WebGL 2 and SharedArrayBuffer are suported. Commented Aug 1, 2017 at 14:13
  • 2
    var sab = new SharedArrayBuffer(4); var lock = new Int32Array(sab); Atomics.wait(lock, 0, 0); results in: VM1347:3 Uncaught TypeError: Atomics.wait cannot be called in this context Commented Aug 1, 2017 at 14:26

2 Answers 2

3

After doing some research, it appears that the choice to prevent the main thread from usage of Atomics.wait() is to avoid synchronous thread blocking, since the main thread deals with user events and page rendering as well as other services, and allowing Atomics.wait() would lead to poor user experience on the web application.

The SharedArrayBuffer API is being followed by the OffscreenCanvas API, which is currently still unimplemented on Chrome, but is available on Firefox.

Using an offscreen canvas, you could Atomics.wait() from within a web worker intended for rendering, apply your gl operations after reading the data from the shared array buffer, and then call gl.commit(), which would render the gl frame to the main thread.

Unfortunately, since Firefox is the only browser to currently support the OffscreenCanvas API, and NW.js is only for Chromium, this particular synchronization challenge does not appear possible to overcome due to lack of support for both Atomics.wait() and WebGL in the same thread on Chrome.

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

2 Comments

Jea i read about that and also made some tests. But the OffscreenCanvas suffers from lots of bugs (for example canvas disappearing after idle for one sec) and as you mention is only available in FF. No luck with that feature at the moment :(
There is another possibility you could check out. There's a synchronization library called parlib that facilitates atomic operations using a polyfill. There are demo programs in the demo/ directory for reference, since there doesn't appear to be very thorough documentation.
2

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.

2 Comments

1. Why? I was not implementing my own "lock mechanism" but used the existing ones. 2. Blocking data from the main thread does not imply that no other thread is already working with the blocked data. Race condition can still appear. 3. Messages have ALOT of overhead and are not viable for real time data flows. 4. In my scenario there needed to be hundreds of consecutive checks each frame at at least 30 frames / s. Again too much overhead.
Updated answer.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.