Search code examples
javascriptmutex

Mutex in JavaScript - does this look like a correct implementation?


Not an entirely serious question, more of a shower thought: JavaScript's await keyword should allow for something that feels an awful lot like a mutex in your average "concurrent language".

function Mutex() {
    var self = this; // still unsure about how "this" is captured
    var mtx = new Promise(t => t()); // fulfilled promise ≡ unlocked mutex
    this.lock = async function() {
        await mtx;
        mtx = new Promise(t => {
            self.unlock = () => t();
        });
    }
}
// Lock
await mutex.lock();
// Unlock
mutex.unlock();

Is this a correct implementation (apart from proper error handling)? And… can I have C++-RAII-style lock guards?


Solution

  • Your implementation allows as many consumers obtain the lock as ask for it; each call to lock waits on a single promise:

    function Mutex() {
        var self = this; // still unsure about how "this" is captured
        var mtx = new Promise(t => t()); // fulfilled promise ≡ unlocked mutex
        this.lock = async function() {
            await mtx;
            mtx = new Promise(t => {
                self.unlock = () => t();
            });
        }
    }
    
    const mutex = new Mutex();
    
    (async () => {
      await Promise.resolve();
      await mutex.lock();
      console.log("A got the lock");
    })();
    (async () => {
      await Promise.resolve();
      await mutex.lock();
      console.log("B got the lock");
    })();

    You'd need to implement a queue of promises, creating a new one for each lock request.

    Side notes:

    • new Promise(t => t()) can be more simply and idiomatically written Promise.resolve() :-)
    • No need for self if you're using arrow functions like that; arrow functions close over the this where they're created (exactly like closing over a variable)
    • Probably would make sense for unlock to be a resolution value of the lock promise, so only the code that obtained the lock can release it

    Something like this:

    function Mutex() {
        let current = Promise.resolve();
        this.lock = () => {
            let _resolve;
            const p = new Promise(resolve => {
                _resolve = () => resolve();
            });
            // Caller gets a promise that resolves when the current outstanding
            // lock resolves
            const rv = current.then(() => _resolve);
            // Don't allow the next request until the new promise is done
            current = p;
            // Return the new promise
            return rv;
        };
    }
    

    Live Example:

    "use strict";
    function Mutex() {
        let current = Promise.resolve();
        this.lock = () => {
            let _resolve;
            const p = new Promise(resolve => {
                _resolve = () => resolve();
            });
            // Caller gets a promise that resolves when the current outstanding
            // lock resolves
            const rv = current.then(() => _resolve);
            // Don't allow the next request until the new promise is done
            current = p;
            // Return the new promise
            return rv;
        };
    }
    
    const rand = max => Math.floor(Math.random() * max);
    
    const delay = (ms, value) => new Promise(resolve => setTimeout(resolve, ms, value));
    
    const mutex = new Mutex();
    
    function go(name) {
        (async () => {
            console.log(name + " random initial delay");
            await delay(rand(50));
            console.log(name + " requesting lock");
            const unlock = await mutex.lock();
            console.log(name + " got lock");
            await delay(rand(1000));
            console.log(name + " releasing lock");
            unlock();
        })();
    }
    go("A");
    go("B");
    go("C");
    go("D");
    .as-console-wrapper {
      max-height: 100% !important;
    }