JavaScript Threading #52
thecodedrift
announced in
Thunked
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
-
There's a lot of digging required to figure out if JavaScript is single or multi-threaded. Thanks to the work Dan Simard in June, a final answer on JavaScript threads was reached. The only major problem in this was that the comments by a user to Simard's post indicated that
alert()
caused Firefox's execution chain to change. The result was while the prompt was on screen,setTimeout()
and other assorted callback functions were free to resolve.The problem is made worse with several
setTimeout()
calls. As a single thread, JavaScript maintains some sort of event stack, where all of the callbacks and timeouts pile up, waiting for their turn. Once the event stack is out of events, the timers start again to count down to execution. If you fire one call with asetTimeout()
and a timeout value of 2 seconds, and one more second of code execution follows, there will still be 2 seconds on the timer since it has not had a chance to run.In a perfectly timed environment (running just this code),
foo
will execute, followed bybar
, followed byfoo
's call tobar
. In reality, the millisecond calibration for this is iffy. The output comes out asfoo=1
,bar=2
thenfoo=1
,bar=1
. However, simply looking at the timing values, our alert calls should be reversed. We've entered the world of volatile variables and shared space without even knowing (and probably not even wanting). Under the hood, it appears that making a call toalert()
,prompt()
,confirm()
, or any other window level call pauses the current thread's execution, letting any pendingsetTimeout()
calls resolve. When the execution returns from the window event, variables that were scoped one block higher could have changed without the knowledge of the current execution block.A more practical example of this can be found on the thread safe
JS Example Page. As of this writing, IE and Opera execute things properly in order, waiting for the prompt() to finish before continuing the current code block, and after completion, letting the pendingsetTimeout()
event fire. In Firefox and Safari, during theprompt()
call, thesetTimeout()
executes, overwriting the value of bar. [Ed-- unfortunately, the test page is currently MIA. If you find it, I'd love to relink it!]Protecting a Volatile Bar
As mentioned before, when the browser's JS Interpreter hands off to the
alert()
, it begins resolving any waitingsetTimeout()
calls that in in the event queue. In our example, another call tobar()
is waiting which also incrementsbar_count
. By the time we alertbar_count
, something has gone and changed it. This seems to also hold true forprompt()
andconfirm()
in Firefox and a few other browsers.Imagine instead of just our primitive
window.setTimeout()
we were using an AJAX callback which waited for a DOM node to be available. The costs of this shared variable space being violated is a bit more costly. In many other languages, there are Mutexes for the synchronized calls, and the volatile declarative to avoid problems. Because the implementation of ECMAScript is unique to each browser, there needs to be a consistent way to secure data from tampering during the application's critical moments.At our disposal...
new Object()
is atomic (can't be interrupted by browser events)return (new Object() === new Object());
is always false as each new object is given a unique ID by the system. We need a way to atomically create an ID for a fake thread, and the Object's deep equality can do just that given it is atomic.Array.push()
is atomic (can't be interrupted by browser events) as long as we are using the native implementation. Even if two objects push onto an array in a race condition, only one will hold the coveted index of0
.(function() { new Object(); })();
is garbage collected properlyThat doesn't leave us with much. Ming made a post on JS Mutexes though that gave me an idea. The biggest concern from Ming's entries was that in Internet Explorer, events such as onload can interrupt a process, creating multiple events at the same time. To get around this, we would need a method which the browser cannot interrupt. This is where the native
Array.push()
comes in handy. For some reason, IE treats this call as non-interruptible, letting us have a queue process that ensures element0
is the first lock requester. If we have two atomic commands, one to uniquely identify a lock request and another to enqueue the lock, then we have everything we need for a very basic spinlock implementation. Even if a second event interrupts our two statements, the pushing onto the array guarantees only one of the two requests will be given rights to the lock. We'll know which function got the lock thanks to our deep quality===
of objects.Spinlock design implies a "busy wait" cycle for getting the lock, meaning each individual thread usually sits in the equivalent of
while(1) {}
. Because we don't have threads and instead have the event queue, a while loop would seize the system. To get around this, we can use the samesetTimeout()
call that was the source of our problem to "sleep" an event until the lock resource is available. The result is a very lazy locking method that requires the application to care about the lock (but a lock that can be used in many different ways). Our pseudo code looks like:Return to the
thread safe test pageand try the protected version. Across all browsers, the lock is held until the our asynchronous events properly completed.Beyond Locking Down Variables
A locking system like this can be extended to deal with more than just the JavaScript Variables. In the case of lazy loading JavaScript, it is possible to be using the same utility to load several "sets" of JavaScript at the same time. To avoid keeping the
document.head
open (and risking Operation Aborted problems), we use the same locking strategy to keep a DOM node from getting written to until Internet Explorer reports it as closed, such as anonreadystate
for a script tags.The locking utility can also be used to create "run once" functions such as an
init()
for a class outside of its constructor. The class can simply request the lock, and on completion, refuse to release it. While this takes up a small bit of memory in the lock utility, it also guarantees only oneinit()
reaches the critical section for the life of the page.Limitations
Building such a locking system in JavaScript is not without drawbacks. The most notable of these is the lack of support for browsers which do not support
Array.push()
(hello, Internet Explorer on a Mac). If you have this esoteric corner case, I'm stumped; there's not much you can do.It's also unknown just how atomic the methods are in non-browsers. While brief tests in Rhino imply that
Array.push()
is properly atomic, there's no guarantee that it is in any specification.Beta Was this translation helpful? Give feedback.
All reactions