Node.js is a framework for writing server-side JavaScript applications. It is built on top of the V8 JavaScript runtime and uses an event-driven, non-blocking I/O model that makes it perfect for data-intensive, real-time applications.
This blog post will describe what non-blocking I/O means and how working with the event loop can help your applications be more efficient.
“This [non-blocking I/O] model simplifies access to slow resources in a scalable way that is intuitive to JavaScript programmers and easy to learn for everyone else.” -
Node Up and Running.
The Restaurant
In order to understand non-blocking I/O, let’s picture a common scenario. Suppose you are at a restaurant with friends. A typical experience at a restaurant would be something like this:
- You sit at a table and the server grabs your drink order.
- The server goes back to the bar and passes your order to a bartender.
- While the bartender is working on your drink, the server moves on to grab another table’s drink order.
- The server goes back to the bar and passes along the other table’s order.
- Before the server brings back your drinks, you order some food.
- Server passes your food order to the kitchen.
- Your drinks are ready now, so the server picks them up and brings them back to your table.
- The other table’s drinks are ready, so the server picks them up and takes them to the other table.
- Finally your food is ready, so server picks it up and brings it back to your table.
A Restaurant server taking orders
Basically every interaction with the server follows the same pattern. First, you order something. Then, the server goes on to process your order and return it to you when it’s ready.
Once the order is handed off to the bar or kitchen, the server is free to get new orders or to deliver previous orders that are completed. Notice that at no point in time is the server doing more than one thing. They can only process one request at a time.
This is pretty much how non-blocking Node.js applications work. In Node, your application code is like a restaurant server processing orders, and the bar/kitchen is the operating system handling your I/O calls.
I/O Calls
Your single-threaded JavaScript application is responsible for all the processing up to the moment it requires I/O. Then, it hands the work off to the operating system which takes care of processing the rest.
What is I/O ?
Once an application has started, it is loaded into the machine’s memory. That’s what the CPU will mostly use for running your program. I/O is shorthand for Input and Output and it means accessing anything outside of your application.
Accessing memory is pretty fast, hence a lot of caching mechanisms simply use RAM to store data. However, applications will often need to access the network or read from a text file, and these types of I/O are by far the slowest types. That’s where non-blocking I/O really shines.
Blocking I/O
Back to our restaurant example, if every time the server got an order request they had to wait for the bar/kitchen to finish before taking the next request, then the service for this restaurant would be very slow and customers would most likely be unsatisfied. This is how blocking I/O works.
Consider the following code example:
// requesting drinks for table 1 and waiting...
var drinksForTable1 = requestDrinksBlocking(['Coke', 'Iced Tea', 'Water']);
// once drinks are ready, then server takes order back to table.
serveOrder(drinksForTable1);
// once order is delivered, server moves on to another table.
// requesting drinks for table 2 and waiting...
var drinksForTable2 = requestDrinksBlocking(['Beer', 'Whiskey', 'Vodka']);
// once drinks are ready, then server takes order back to table.
serveOrder(drinksForTable2);
// once order is delivered, server moves on to another table.
// requesting food for table 1 and waiting..
var foodForTable1 = requestFoodBlocking(['Hamburger', 'Salad', 'Pizza']);
// once food is ready, then server takes order back to table.
serveOrder(foodForTable1);
// once order is delivered, server moves on to another table.
In the previous example, both the requestDrinksBlocking and requestFoodBlocking functions perform some sort of blocking I/O call, and the whole process stops while it waits for the I/O operation to finish. Good thing this isn’t how most restaurants operate!
Non-Blocking I/O
Our previous example re-written for a non-blocking I/O model allows for multiple I/O calls to be performed without halting the execution of the program. Instead, these calls run independently.
// requesting drinks for table 1 and moving on...
requestDrinksNonBlocking(['Coke', 'Iced Tea', 'Water'], function(drinks){
return serveOrder(drinks);
});
// requesting drinks for table 2 and moving on...
requestDrinksNonBlocking(['Beer', 'Whiskey', 'Vodka'], function(drinks){
return serveOrder(drinks);
});
// requesting food for table 1 and moving on...
requestFoodNonBlocking(['Hamburger', 'Salad', 'Pizza'], function(food){
return serveOrder(food);
});
The callback functions passed as arguments are invoked asynchronously when the return value from their respective I/O calls are available. This looks like a much better restaurant to go to!
The Event Loop
Now that we’ve seen how to write non-blocking Node.js code, let’s take a look at the missing piece of the puzzle. This is the part responsible for actually invoking the callbacks: the event loop.
When non-blocking I/O calls are complete and their return value is available, they emit an event.
Node’s approach to handling these events leverages a well known feature in JavaScript: functions as first class citizens. As we’ve seen in our previous example, we simply pass functions as callback arguments to Node’s non-blocking functions. These callbacks are invoked when the return value from their current I/O calls are available and the proper events are emitted.
Suppose there is a ready event that is emitted when an order is ready. Another way to write the previous code would be to explicitly listen to the ready event:
// requesting drinks for table 1 and listening to the 'ready' event.
var requestDrinksTable1 = requestDrinksNonBlocking(['Coke', 'Iced Tea', 'Water']);
requestDrinksTable1.on('ready', function(drinks){
return serveOrder(drinks);
});
// requesting drinks for table 2 and listening to the 'ready' event.
var requestDrinksTable2 = requestDrinksNonBlocking(['Beer', 'Whiskey', 'Vodka']);
requestDrinksTable2.on('ready', function(drinks){
return serveOrder(drinks);
});
// requesting food for table 1 and listening to the 'ready' event.
var requestFoodTable1 = requestFoodNonBlocking(['Hamburger', 'Salad', 'Pizza']);
requestFoodTable1.on('ready', function(food){
return serveOrder(food);
});
In Node, all objects that listen to or emit events, inherit from EventEmitter.
Blocking the Event Loop
Applications written in Node run on a single-thread. This means that they can only do one thing at a time, just like our restaurant server.
Let’s say that when your food order is ready, the server needs to count the total number of beans in your lentil soup:
requestFoodNonBlocking('Lentil Soup', function(soup){
countNumberOfBeans(soup); //blocks...
return serveOrder(soup);
});
The server is able to pass along your food order to the kitchen in a non-blocking fashion and then proceed to do other things. However, once your order is back, they start to count the number of beans in your soup and simply block. They won’t be able to do anything else until they are done counting the beans.
If for some reason countNumberOfBeans(soup) results in an infinite loop, then the server is stuck there forever and you will be forced to look for food somewhere else.
Conclusion
Writing server-side JavaScript in Node.js is easy and fun. It doesn’t take much to get up to speed and start writing efficient, non-blocking applications that can handle heavy traffic.
Being single-threaded means that Node.js programs can only do one thing at a time, so it is important to understand how to properly work with the event loop in order to take full advantage of the platform and avoid common pitfalls. For applications that require heavy computation and not much I/O, Node might not be your best choice.
If you want to learn more about Node.js, be sure to check out the Real-time Web with Node.js course over at Code School.
Also, Jacob Swanner and I will be teaching a Node.js workshop next week at O’Reilly Fluent Conference in San Francisco. Come and say hi if you are there!
- Carlos Souza