111

How could something equivalent to lock in C# be implemented in JavaScript?

So, to explain what I'm thinking a simple use case is:

User clicks button B. B raises an onclick event. If B is in event-state the event waits for B to be in ready-state before propagating. If B is in ready-state, B is locked and is set to event-state, then the event propagates. When the event's propagation is complete, B is set to ready-state.

I could see how something close to this could be done, simply by adding and removing the class ready-state from the button. However, the problem is that a user can click a button twice in a row faster than the variable can be set, so this attempt at a lock will fail in some circumstances.

Does anyone know how to implement a lock that will not fail in JavaScript?

Brian Tompsett - 汤莱恩
  • 5,753
  • 72
  • 57
  • 129
smartcaveman
  • 41,281
  • 29
  • 127
  • 212
  • 1
    Answers that take into account parallel JS execution (i.e. like in IE9) would be appreciated. – OrangeDog Mar 18 '11 at 00:02
  • @smart So you want to temporarily pause the current event propagation until the propagation of a previous event is complete. I don't think that that can be done. You could do this: discard the event and then fire another one when the previous event has finished propagating. – Šime Vidas Mar 18 '11 at 00:47
  • @OrangeDog - I have only heard IE9 tries to use a dedicated core for compilation, nothing about parallel execution, can you cite a source? – Brandon Mar 18 '11 at 00:55
  • JavaScript is single threaded apart from web workers. Web workers shouldn't require a locking solution to be set up. – Raynos Mar 18 '11 at 01:42
  • @Brandon - "The Chakra engine interprets, compiles, and executes code in parallel and takes advantage of multiple CPU cores, when available." -- http://msdn.microsoft.com/en-us/ie/ff468705.aspx – OrangeDog Mar 18 '11 at 13:53
  • 1
    @Brandon - That might mean, as you suggest, parallel to the renderer, rather than parallel to itself. – OrangeDog Mar 18 '11 at 13:54
  • 1
    Related: http://stackoverflow.com/questions/6266868/microsoft-says-ie9-has-parallel-javascript-rendering-and-execution – David Murdoch Jun 07 '11 at 14:35

9 Answers9

99

Lock is a questionable idea in JS which is intended to be threadless and not needing concurrency protection. You're looking to combine calls on deferred execution. The pattern I follow for this is the use of callbacks. Something like this:

var functionLock = false;
var functionCallbacks = [];
var lockingFunction = function (callback) {
    if (functionLock) {
        functionCallbacks.push(callback);
    } else {
        $.longRunning(function(response) {
             while(functionCallbacks.length){
                 var thisCallback = functionCallbacks.pop();
                 thisCallback(response);
             }
        });
    }
}

You can also implement this using DOM event listeners or a pubsub solution.

Steven Schobert
  • 4,201
  • 3
  • 25
  • 34
JoshRivers
  • 9,920
  • 8
  • 39
  • 39
  • 27
    could you please you provide reference documentation justifying your statement that "JS is intended to be threadless and not needing concurrency protection"? I would like to read more about this. – smartcaveman Jun 05 '11 at 00:02
  • 2
    +1 - Given that Node.js (JavaScript web server) was built to take advantage of JavaScript's lone-thread and callback mechanism, I'd agree that you need not worry about locking a property as there will be no race condition. – Fenton Jun 08 '11 at 15:05
  • 1
    What about first function that come first and should be pushed directly to the callback queue and turn functionLock variable true? – Kadir Ercetin Jan 25 '16 at 13:48
  • 5
    What library is $.longRunning from? It's not in (current) jQuery. – Quantum7 May 10 '17 at 11:32
  • 5
    when functionLock supposed to be set? – Elton Santana Jun 21 '17 at 18:59
  • 26
    "JS which is intended to be threadless and not needing concurrency" is dubious. JS does not use preemptive concurrency but does allow you to use cooperative concurrency (callback or async/await based). You can definitely have race conditions when using those and might want to use a mutex abstraction to avoid them. – ysdx Feb 14 '18 at 08:56
  • Web Workers and WebAssembly are also a thing. See @Wickramaranga's answer https://stackoverflow.com/a/73482349/481816 – Dude0001 Aug 10 '23 at 17:17
46

JavaScript is, with a very few exceptions (XMLHttpRequest onreadystatechange handlers in some versions of Firefox) event-loop concurrent. So you needn't worry about locking in this case.

JavaScript has a concurrency model based on an "event loop". This model is quite different than the model in other languages like C or Java.

...

A JavaScript runtime contains a message queue, which is a list of messages to be processed. To each message is associated a function. When the stack is empty, a message is taken out of the queue and processed. The processing consists of calling the associated function (and thus creating an initial stack frame) The message processing ends when the stack becomes empty again.

...

Each message is processed completely before any other message is processed. This offers some nice properties when reasoning about your program, including the fact that whenever a function runs, it cannot be pre-empted and will run entirely before any other code runs (and can modify data the function manipulates). This differs from C, for instance, where if a function runs in a thread, it can be stopped at any point to run some other code in another thread.

A downside of this model is that if a message takes too long to complete, the web application is unable to process user interactions like click or scroll. The browser mitigates this with the "a script is taking too long to run" dialog. A good practice to follow is to make message processing short and if possible cut down one message into several messages.

For more links on event-loop concurrency, see E

Community
  • 1
  • 1
Mike Samuel
  • 118,113
  • 30
  • 216
  • 245
  • And with [Web Workers(non UI threads)](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers) in javascript, each object [clones](https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm) before send to another worker(thread) or UI thread then the object have different instances in different threads and each thread have owned event loop and finally I agree with Mike's answer as a helpful answer. Thanks Mike. – Ehsan Mohammadi Mar 21 '19 at 19:26
23

I've had success mutex-promise.

I agree with other answers that you might not need locking in your case. But it's not true that one never needs locking in Javascript. You need mutual exclusivity when accessing external resources that do not handle concurrency.

Tim Scott
  • 15,106
  • 9
  • 65
  • 79
12

If it helps anyone in 2022+, all major browsers now support Web Locks API although experimental.

To quote the example in MDN:

await do_something_without_lock();

// Request the lock.
await navigator.locks.request('my_resource', async (lock) => {
  // The lock has been acquired.
  await do_something_with_lock();
  await do_something_else_with_lock();
  // Now the lock will be released.
});
// The lock has been released.

await do_something_else_without_lock();
  • Lock is automatically released when the callback returns
  • Locks are scoped to origins (https://example.com != https://example.org:8080), and work across tabs/workers.
  • Lock requests are queued (first come-first served); (unlike in some other languages where the lock is passed to some thread at random)
  • navigator.locks.query() can be used to see what has the lock, and who are in the queue to acquire the lock
  • There is a mode="shared" to implement a readers-writers lock if you need it
Wickramaranga
  • 943
  • 2
  • 19
  • 35
6

Locks are a concept required in a multi-threaded system. Even with worker threads, messages are sent by value between workers so that locking is unnecessary.

I suspect you need to just set a semaphore (flagging system) between your buttons.

James Westgate
  • 11,306
  • 8
  • 61
  • 68
  • 1
    do you have any such examples or resources for a 'semaphore/flagging system' implemented in JavaScript ? – smartcaveman Jun 08 '11 at 22:24
  • 4
    PLenty right here on SO ie http://stackoverflow.com/questions/4194346/how-to-create-semaphore-between-html-elements-loaded-async – James Westgate Jun 09 '11 at 09:09
  • James I don’t see any semaphore examples in the link you posted. I need to prevent calls to an external server because they only allow one request per second, so I need a flagging example that would prevent a call until the elapsed time has passed inside my for next loop without skipping a request for each of the loops. – Claus Apr 18 '20 at 23:03
  • Hi @Claus. Two things spread to mind, depending on your stack, you could just use a library like Axios which seems to have support for a rate limit plugin, or perhaps you could create a webworker which rate limits requests based on domain. – James Westgate Apr 20 '20 at 12:48
  • Thanks for the response. I got it resolved by calling the click event at end of processing in combination with setInterval. Setting the interval to 1000 milliseconds I now obey the host server limit on requests and it no longer exits the Ajax call before the response is returned. – Claus Apr 22 '20 at 14:24
4

Here's a simple lock mechanism, implemented via closure

const createLock = () => {

    let lockStatus = false

    const release = () => {
        lockStatus = false
    }

    const acuire = () => {
        if (lockStatus == true)
            return false
        lockStatus = true
        return true
    }
    
    return {
        lockStatus: lockStatus, 
        acuire: acuire,
        release: release,
    }
}

lock = createLock() // create a lock
lock.acuire() // acuired a lock

if (lock.acuire()){
  console.log("Was able to acuire");
} else {
  console.log("Was not to acuire"); // This will execute
}

lock.release() // now the lock is released

if(lock.acuire()){
  console.log("Was able to acuire"); // This will execute
} else {
  console.log("Was not to acuire"); 
}

lock.release() // Hey don't forget to release
user3841707
  • 69
  • 1
  • 3
2

Why don't you disable the button and enable it after you finish the event?

<input type="button" id="xx" onclick="checkEnableSubmit('true');yourFunction();">

<script type="text/javascript">

function checkEnableSubmit(status) {  
  document.getElementById("xx").disabled = status;
}

function yourFunction(){

//add your functionality

checkEnableSubmit('false');
}

</script>

Happy coding !!!

Grzegorz Rożniecki
  • 27,415
  • 11
  • 90
  • 112
Ravi Vanapalli
  • 9,805
  • 3
  • 33
  • 43
0

Some addition to JoshRiver's answer according to my case;

var functionCallbacks = [];
    var functionLock = false;
    var getData = function (url, callback) {
                   if (functionLock) {
                        functionCallbacks.push(callback);
                   } else {
                       functionLock = true;
                       functionCallbacks.push(callback);
                        $.getJSON(url, function (data) {
                            while (functionCallbacks.length) {
                                var thisCallback = functionCallbacks.pop();
                                thisCallback(data);
                            }
                            functionLock = false;
                        });
                    }
                };

// Usage
getData("api/orders",function(data){
    barChart(data);
});
getData("api/orders",function(data){
  lineChart(data);
});

There will be just one api call and these two function will consume same result.

Kadir Ercetin
  • 869
  • 8
  • 11
0

Locks still have uses in JS. In my experience I only needed to use locks to prevent spam clicking on elements making AJAX calls. If you have a loader set up for AJAX calls then this isn't required (as well as disabling the button after clicking). But either way here is what I used for locking:

var LOCK_INDEX = [];
function LockCallback(key, action, manual) {
    if (LOCK_INDEX[key])
        return;
    LOCK_INDEX[key] = true;
    action(function () { delete LOCK_INDEX[key] });
    if (!manual)
        delete LOCK_INDEX[key];
}

Usage:

Manual unlock (usually for XHR)

LockCallback('someKey',(delCallback) => { 
    //do stuff
    delCallback(); //Unlock method
}, true)

Auto unlock

LockCallback('someKey',() => { 
    //do stuff
})
Bodokh
  • 976
  • 4
  • 15
  • 34