- The Old Way of Doing Things
- The Node.js Way of Doing Things
- Error Handling and Asynchronous Functions
- Who Am I? Maintaining a Sense of Identity
- Being Polite-Learning to Give Up Control
- Synchronous Function Calls
- Summary
Being Polite—Learning to Give Up Control
Node runs in a single thread with a single event loop that makes calls to external functions and services. It places callback functions on the event queue to wait for the responses and otherwise tries to execute code as quickly as possible. So what happens if you have a function that tries to compute the intersection between two arrays:
function compute_intersection(arr1, arr2, callback) { var results = []; for (var i = 0 ; i < arr1.length; i++) { for (var j = 0; j < arr2.length; j++) { if (arr2[j] == arr1[i]) { results[results.length] = arr1[j]; break; } } } callback(null, results); // no error, pass in results! }
For arrays of a few thousand elements, this function starts to consume significant amounts of time to do its work, on the order of a second or more. In a single-threaded model, where Node.js can do only one thing at a time, this amount of time can be a problem. Similar functions that compute hashes, digests, or otherwise perform expensive operations are going to cause your applications to temporarily “freeze” while they do their work. What can you do?
In the Introduction to this book, I mentioned that there are certain things for which Node.js is not particularly well suited, and one of them is definitely acting as a compute server. Node is far better suited to more common network application tasks, such as those with heavy amounts of I/O and requests to other services. If you want to write a server that does a lot of expensive computations and calculations, you might want to consider moving these operations to other services that your Node applications can then call remotely.
I am not saying, however, that you should completely shy away from computationally intensive tasks. If you’re doing these only some of the time, you can still include them in Node.js and take advantage of a method on the process global object called nextTick. This method basically says, “Give up control of execution, and then when you have a free moment, call the provided function.” It tends to be significantly faster than just using the setTimeout function.
Listing 3.2 contains an updated version of the compute_intersection function that yields every once in a while to let Node process other tasks.
Listing 3.2. Using process#nextTick to Be Polite
function compute_intersection(arr1, arr2, callback) { // let's break up the bigger of the two arrays var bigger = arr1.length > arr2.length ? arr1 : arr2; var smaller = bigger == arr1 ? arr2 : arr1; var biglen = bigger.length; var smlen = smaller.length; var sidx = 0; // starting index of any chunk var size = 10; // chunk size, can adjust! var results = []; // intermediate results // for each chunk of "size" elements in bigger, search through smaller function sub_compute_intersection() { for (var i = sidx; i < (sidx + size) && i < biglen; i++) { for (var j = 0; j < smlen; j++) { if (bigger[i] == smaller[j]) { results.push(smaller[j]); break; } } } if (i >= biglen) { callback(null, results); // no error, send back results } else { sidx += size; process.nextTick(sub_compute_intersection); } } sub_compute_intersection(); }
In this new version of the function, you basically divide the bigger of the input arrays into chunks of 10 (you can choose whatever number you want), compute the intersection of that many items, and then call process#nextTick to allow other events or requests a chance to do their work. Only when there are no events in front of you any longer will you continue to do the work. Don’t forget that passing the callback function sub_compute_intersection to process#nextTick ensures that the current scope is preserved as a closure, so you can store the intermediate results in the variables in compute_intersection.
Listing 3.3 shows the code you use to test this new compute_intersection function.
Listing 3.3. Testing the compute_intersection Function
var a1 = [ 3476, 2457, 7547, 34523, 3, 6, 7,2, 77, 8, 2345, 7623457, 2347, 23572457, 237457, 234869, 237, 24572457524] ; var a2 = [ 3476, 75347547, 2457634563, 56763472, 34574, 2347, 7, 34652364 , 13461346, 572346, 23723457234, 237, 234, 24352345, 537, 2345235, 2345675, 34534, 7582768, 284835, 8553577, 2577257,545634, 457247247, 2345 ]; compute_intersection(a1, a2, function (err, results) { if (err) { console.log(err); } else { console.log(results); } });
Although this has made things a bit more complicated than the original version of the function to compute the intersections, the new version plays much better in the single-threaded world of Node event processing and callbacks, and you can use process.nextTick in any situation in which you are worried that a complex or slow computation is necessary.