javascriptmutex

Mutex in JavaScript - promise based, but lock is not exclusive?


Thinking that JavaScript's await keyword should allow for something that feels an awful lot like a mutex in your average "concurrent language", I tried the following:

function Mutex() {
    var self = this; 
    var mtx = new Promise(t => t());
    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");
})();

But this doesn't seem to work as I expected. Despite that the lock method awaits the mtx promise, both A and B get the lock in the above snippet.

What is wrong here?


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:

    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;
    }