JavaScript Concurrency Model

Dr. Greg Bernstein

Updated March 18th, 2021

Concurrency

Learning Objectives

  • Understand JavaScript’s Runtime model
  • Review methods for dealing with concurrency
  • Use the event queue with callbacks and understand issues with this approach

Readings/References

JavaScript Runtime Model

From MDN:

Run time model

Key Ingredients

  • Stack: Memory used for function calls. Stack frames consist of function arguments and local variables.

  • Heap: Memory used for all variables and objects.

  • Message/Event Queue: Mechanism for JavaScript concurrency.

Common Concurrency Mechanisms

  • Processes: Implemented by an OS, processes get separate protected memory spaces. Highest in overhead.

  • Threads: Implemented by runtime, OS, or library. Threads get separate program counters. Memory can be shared between threads. Not easy to program with in general.

  • Review Threads and Processes

Queue

From MDN:

“A JavaScript runtime uses a message queue, which is a list of messages to be processed. Each message has an associated function which gets called in order to handle the message.”

Event Loop

Psuedo-code model from MDN:

while (queue.waitForMessage()) {
  queue.processNextMessage();
}

The previous code actually needs thread-like functionality to work.

Processing Events/Messages

  • The queue is ordered structure like a “waiting line”

  • Processing an event/message involves calling the callback function with parameters corresponding to the event or message.

  • The callback function is “run-to-completion”. This is known as non-preemptive scheduling. Review scheduling in OS

Adding Messages to the Queue

From MDN:

“In web browsers, messages are added anytime an event occurs and there is an event listener attached to it. If there is no listener, the event is lost.”

Example Events

  • DOM events
    1. GUI events
    2. Network on/offline, Websocket events
    3. And more…
  • Implicit Events (my terminology)
    1. When a fetch call returns.
    2. When a timer/interval expires.
    3. And more…

Make your Own Event

Using the setTimeout() function

  • var timeoutID = scope.setTimeout(callback[, delay, param1, param2, ...]);
  • Adds a “message” (the parameters), and callback function to the queue after delay in milliseconds.
  • delay is optional and defaults to 0.

Example

Modified from MDN:

console.log('this is the start');

setTimeout(function cb1() {
  console.log('this is a msg from call back 1');
});

console.log('this is just a message');

setTimeout(function cb2() {
  console.log('this is a msg from call back 2');
}, 0);

console.log('this is the end');

Results

timeout results

Order and Asynchronous Operations

Silly Clock I

badTimes.js What will this do?

myTime = 0.0;
startTime = new Date();

function advanceTime() {
    myTime += 1.0;
    elapsedTime = (new Date() - startTime)/1000.0;
    console.log(`myTime = ${myTime}, elapsedTime = ${elapsedTime}`);
}
// What will this do?
setTimeout(advanceTime, 1000);
setTimeout(advanceTime, 1000);
setTimeout(advanceTime, 1000);
setTimeout(advanceTime, 1000);

Silly Clock I: Result

Not really a good clock by any measure…

$ node badTimes.js
myTime = 1, elapsedTime = 1.001
myTime = 2, elapsedTime = 1.003
myTime = 3, elapsedTime = 1.003
myTime = 4, elapsedTime = 1.004

Silly Clock II

goodTimes.js: Nest those calls!

myTime = 0.0;
startTime = new Date();
// What will this do?
setTimeout(function(){
        myTime += 1.0;
        elapsedTime = (new Date() - startTime)/1000.0;
        console.log(`myTime = ${myTime}, elapsedTime = ${elapsedTime}`);
        setTimeout(function(){
            myTime += 1.0;
            elapsedTime = (new Date() - startTime)/1000.0;
            console.log(`myTime = ${myTime}, elapsedTime = ${elapsedTime}`);
            setTimeout(function(){
                myTime += 1.0;
                elapsedTime = (new Date() - startTime)/1000.0;
                console.log(`myTime = ${myTime}, elapsedTime = ${elapsedTime}`);
                setTimeout(function(){
                    myTime += 1.0;
                    elapsedTime = (new Date() - startTime)/1000.0;
                    console.log(`myTime = ${myTime}, elapsedTime = ${elapsedTime}`);
                }, 1000);
            }, 1000);
        }, 1000);
    }, 1000);

Silly Clock II: Result

It counts! But not pretty

$ node goodTimes.js
myTime = 1, elapsedTime = 1.001
myTime = 2, elapsedTime = 2.005
myTime = 3, elapsedTime = 3.009
myTime = 4, elapsedTime = 4.01

Even More Extreme

From goodTimesMore.js

// Find the "pyramid of doom"
let myTime = 0.0;
let startTime = new Date();
function updateTime() {
    myTime += 1.0;
    elapsedTime = (new Date() - startTime) / 1000.0;
    console.log(`myTime = ${myTime}, elapsedTime = ${elapsedTime}`);
}

// What will this do?
setTimeout(function() {
    updateTime();
    setTimeout(function() {
        updateTime();
        setTimeout(function() {
            updateTime();
            setTimeout(function() {
                updateTime();
                setTimeout(function() {
                    updateTime();
                    setTimeout(function() {
                        updateTime();
                        setTimeout(function() {
                            updateTime();
                            setTimeout(function() {
                                updateTime();
                            }, 1000);
                        }, 1000);
                    }, 1000);
                }, 1000);
            }, 1000);
        }, 1000);
    }, 1000);
}, 1000);

Issues

  • Ugly deeply nested code! And we didn’t deal with error conditions. When this gets deeper and more complicated it is known as “callback hell” or the “pyramid of doom”.

  • Alternative Approach: JavaScript Promises

// reveal.js plugins