Concurrency
Last week was an introduction to databases, but I neglected to cover an important topic to understanding how to work with them. The facilities necessary to efficiently wait for events outside of the program is an important element of any language, and this is typically known as concurrency. You've most likely touched on the topic in previous classes, but we're going to build the idea up from first principles so you have a frame of reference when I discuss how Javascript handles this concept.
Inefficient Approach
Suppose we want to implement a sleep function for Javascript. A naive implementation of the function could look something like this:
function sleep(seconds) {
const start = new Date();
while((new Date() - start) / 1000 < seconds) {
console.log("sleeping");
}
}
This could indeed solve the problem, but if you ran this code you'd see an immense number of logs even for a single second. That alone might seem not that bad, but this loop is actively waiting for the event to occur, which is preventing other processes in the system from running. This is typically known as busy waiting.
First Class Functions
To understand how Javascript solves this problem, it's important to understand the notion of a first class function. Consider a simple function like this:
function double(x) {
return 2 * x;
}
Of course, the typical way you call this is double(2), but in Javascript you can
also assign it to a variable like this:
const times_two = double;
times_two(2);
This ability to treat functions like variables is a feature of some programming language called "first class functions". The general idea is that functions are just like any other object and can therefore be treated as such.
I believe I covered a use of map in a code review earlier this semester. It's a good
example of a practical application of a first class function, it takes a function and returns a
new list containing the result of the function applied to each element on the list.
With the double function, if you called [1, 2, 3].map(double) it
would result in [2, 4, 6]. There are a whole host of functions for dealing with
lists that involve first class functions; most of them come from Lisp, but the technique is
powerful enough that they've permeated most languages by now in some form.
Often times, you only need the function in the place where it's passed in, so when ECMAScript
6 was relesed in 2015 they introduced something called lambda syntax. Instead of defining double, that means you could define it with the call to map directly like this:
[1, 2, 3].map((x) => 2 * x) It's also possible in more complicated scenarios to have a multi-line call. In this case you
need to explicitly call return like a normal function:
[1, 2, 3].map((x) => {
return 2 * x;
})
One thing to be aware of is that this means you can't use curly braces to produce objects without the explicit return syntax. Intuitively, you may want to do something like this:
[1, 2, 3].map((x) => { original: x, double: 2 * x }) Because this conflicts with the multi-line syntax you instead need to do something like this:
[1, 2, 3].map((x) => {
return { original: x, double: 2 * x };
})
Promises
Now let's return to our sleep example. The tool in Javascript for doing something
after a fixed period if time is
setTimeout. It takes a first class function and a period of time and calls the provided function after
that period of time has elapsed.
setTimeout(() => console.log("delay completed"), 1000); So we have a way of executing something after the desired time period. We could put all of the subsequent functionality in that callback, but this can get out of control rather quickly. Consider if we want to wait 1 second between a series of actions, the code would look something like this:
setTimeout(() => {
console.log("1");
setTimeout(() => {
console.log("2");
setTimeout(() => {
console.log("3");
}, 1000);
}, 1000);
}, 1000);
As you can see, this can quickly get out of control, it's known as the Pyramid of Doom. To get ourselves out of it, I'm first going to introduce something that will seem like it will make things worse. Promises in Javascript are a more general form of this callback model, again using first class functions.
Whereas setTimeout allows you to execute code after a specific event, a
Promise
makes it possible to convert any block of code into one that supports callbacks. To understand
this, here's how we could rewrite our sleep function using a
Promise:
function sleep(seconds) {
return new Promise((resolve) => {
setTimeout(resolve, seconds * 1000);
});
}
Now this code returns right away, but instead of returning nothing it returns a Promise. You can then attach code to the promise to execute when it completes, like this:
sleep(1).then(() => console.log("completed")) Now let's return to our Pyramid of Doom and see how this doesn't help things at all:
sleep(1).then(() => {
console.log("1");
sleep(1).then(() => {
console.log("2");
sleep(1).then(() => {
console.log("3");
});
});
});
I suppose it's not quite as bad, the time isn't pushed out of sight and you can specify in the
more intuitive seconds. But a Promise gets us closer to breaking out of the pyramid
altogether.
Asynchronous Functions
The await keyword allows us to hold execution until a Promise is resolved.
So we could also write the code above like this:
await sleep(1);
console.log("1");
await sleep(1);
console.log("2");
await sleep(1);
console.log("3");
If you've been following along in a console somewhere, this one probably won't work
particularly well out of the box. We can wrap the lines in a function to solve the problem,
but calling await in functions is only valid if the function uses the
async keyword:
async function sleep_demo() {
await sleep(1);
console.log("1");
await sleep(1);
console.log("2");
await sleep(1);
console.log("3");
}
What the async keyword is doing is wrapping the return value of
sleep_demo
in a Promise without you ever having to create one directly. So when you call it,
you should also use the await keyword:
await sleep_demo(); This makes a certain amount of sense, the code waiting for a Promise to resolve
must itself be a Promise. This is the modern way of dealing with concurrency, so
you'll see async and await pop up all over Javascript code.
Databases
How this applies to the assignment you started last week is that databases are all about calls to external systems. As soon as you call a query, the code is stuck waiting for it to complete. If this were solved using busy waiting, the web server would be unable to field additional requests until the query finished.