Write a simple lock

Kelvin Wong
3 min readMar 4, 2018

--

Last week I was in my office trying to write a simple lock in Angular 1.

Why you use Angular 1?

As everyone knows, Angular 1 is probably notorious about its dreadful performance in handling data binding and debugging. Whether we should migrate to Angular 5 is a story for another time.

Why JS needs locks? Isn’t JS single-threaded?

So last week I was dealing with a message queue and I wanted to ensure that the queue can get copied and cleared before any new messages can be pushed into the queue. I don’t want any new messages flooded in before I cleared the queue. My senior told me that Javascript is single-threaded so we should not worry about it. But I doubt.

Javascript is not single-threaded. In fact it can be event-driven. Simultaneously multiple lines of code can run together (sort of) to respond to different events.

Prove: Set a breakpoint in Google Chrome inside a button click event listener. Click a button on a website with real-time update (or polling). Keep clicking F10. Instead of checking JS code line by line, you may see it jump over here and there. That’s because JS can be triggered by any user event and I believe JS can achieve (sort of) multi-threading by time sharing.

OK. back to our business. That’s why we need JS lock.

First lock attempt: ensure only one function running

My first lock attempt only wants to check whether data gets updated. If yes, it will signal the function to make another run based on the new data.

var threads = {};
var lock = function (id, cb) {
var release = function () {
threads[id].locked = false;
if (threads[id].waiting) {
threads[id].waiting = false;
lock(id, cb);
}
}
if (!(id in threads))
threads[id] = { locked: false, waiting: false };
if (!threads[id].locked) {
threads[id].locked = true;
cb(release);
} else {
threads[id].waiting = true;
}
}

You may already find out that this code snippet does not store any additional data or useful arguments. It just signal the script to run again as long as the current running function finishes.

Second lock attempt: store data with timeout

I think this is a solution you may find in many Github repositories — a lock with $timeout or, in vanilla JS terms, setTimeout.

var promiseThreads = {};
var getPromiseThread = function (id) {
if (!(id in promiseThreads)) promiseThreads[id] = 0;
return !(promiseThreads[id] || ((promiseThreads[id] = 1) - 1));
}
var lockPromise = function(id) {
var d = $q.defer();
$timeout(function() {
if (getPromiseThread(id)) d.resolve();
else d.reject();
}, 500);
var p = d.promise;
p.finally(function() {
promiseThreads[id] = 0;
});
return p;
};

I think I should explain a little bit about:
!(promiseThreads[id] || ((promiseThreads[id] = 1) - 1))
because I want to run the following snippet in one line so that this JS code can be run atmoically.

if (promiseThreads[id] == 1)    // thread is occupied
return false;
else { // thread is empty
promiseThreads[id] = 1;
return true;
}

But problem comes when your website runs with real-time update. When the website needs frequent update, $timeout is time-consuming and it becomes the bottleneck of your UI workflow. There will be a constant chrome alert saying:alert: $timeout takes 254 ms, and you know that is not a good sign, especially when you client can see this in Google Chrome developer console. Urgh.

Third lock attempt: store function into array

My senior then reminded me with the simplest and easiest way of handling locks — store function into vanilla JS array.

var threads = {}
var lock = function(id, cb) {
if (!(id in threads)) threads[id] = {lock: false, queue: []};
threads[id].queue.push(cb);
if (threads[id].lock) return;
threads[id].lock = true;
while (threads[id].queue.length > 0)
threads[id].queue.shift()();
threads[id].lock = false;
}

For anyone who is also familiar about JS, you may argue that this code snippet is not thread-safe. Yes, there could be a (very) small chance that two JS threads run together and the second thread just enter the lock simultaneously. (Correct me if I am wrong.) But what makes this so beautiful, genius, elegant and neat is because the queue always pops sequentially and executes until the queue is cleared. And because the queue is shifted and run in one sentence, any function running before their later-added functions is not possible.

Not sure whether this will benefit anyone. Do comment if you have any more great ideas.

--

--