Skip to content

Instantly share code, notes, and snippets.

@montanaflynn
Last active October 20, 2023 06:46
Show Gist options
  • Star 81 You must be signed in to star a gist
  • Fork 28 You must be signed in to fork a gist
  • Save montanaflynn/cb349fd109b561c35d6c8500471cdb39 to your computer and use it in GitHub Desktop.
Save montanaflynn/cb349fd109b561c35d6c8500471cdb39 to your computer and use it in GitHub Desktop.
Examples of sequential, concurrent and parallel requests in node.js

Concurrency in JavaScript

Javascript is a programming language with a peculiar twist. Its event driven model means that nothing blocks and everything runs concurrently. This is not to be confused with the same type of concurrency as running in parallel on multiple cores. Javascript is single threaded so each program runs on a single core yet every line of code executes without waiting for anything to return. This sounds weird but it's true. If you want to have any type of sequential ordering you can use events, callbacks, or as of late promises.

Let's see an example:

function delayedLog(index, delay){
  setTimeout(function(){ 
    console.log("Logging from function call #"+index)
  }, delay)
}

delayedLog(1, 2000)
delayedLog(2, 1000)

Now you might be wondering what setTimeout is doing and it's basically saying run this function after this much time. Javascript is a Higher-order programming language which means you can pass functions as arguments. The first delayedLog will print to stdout after 2 seconds. The second one will print after 1 second.

What you might expect to see is:

// ...wait for 2 seconds
Logging from function call #1
// ...wait for 1 second
Logging from function call #2

But that is not the case, both will be fired at the same time one after the other so the output would be like this:

// ...wait for 1 second
Logging from function call #2
// ...wait for 2 seconds
Logging from function call #1

The most traditional way of handling concurrency where you needed to run functions in a certain order is known as callbacks. This is when you pass a function to a be run at a later time and given arguments that can be acted upon then. That is what setTimeout is doing when you pass it a function.

function log(err, msg) {
  if (err) panic(err)
  console.log(msg)
}

function doSomething(arg, callback) {
  if (isError()) callback("An error happened", null)
  setTimeout(callback(null, arg.reverse), 1000)
}

Now let's look at some real world examples of concurrency. We'll be making a function that sends multiple HTTP requests, first by the function which takes a callback and then invoking that function sequentially, concurrently and finally in parallel with the help of the cluster module.

For reference here is what I mean by sequential, concurrent and parallel:

  • Sequential: do this and then do that
  • Concurrent: do this and do that without waiting between
  • Parallel: do this and do that at the exact same time
// send the request with a request-id header read the body
// simulated delays in responses between .3 and .6 seconds
function sendRequest(requestID, cb){
  var delay = Math.floor(Math.random() * (6 - 3 + 1)) + 5
  var request = {
    hostname: 'httpbin.org',
    headers: {'request-id': requestID},
    path: '/delay/.'+  delay
  }
  require('http').get(request, (res) => {
      var body = ''
      res.on('data', function (chunk) {
        body += chunk
      })
      res.on('end', function () {
        cb(res, body)
      })
  }).end()
}

// SEQUENTIAL REQUESTS
// Have to try a little bit
sequentialRequests(5)
function sequentialRequests(count, i){
  if (i == undefined) {i = 0}
  if (i++ >= count) {return}
  sendRequest(i, function(res, body){
    var reqID = JSON.parse(body).headers['Request-Id']
    var sCode = res.statusCode
    console.log("Sequential Response #"+reqID+" returned a "+sCode)
    sequentialRequests(count, i)
  })
}

// CONCURRENT REQUESTS
// Default by nature of js
concurrentRequests(5)
function concurrentRequests(limit){
  for (var i = 0; i < limit; i++) {
    sendRequest(i, function(res, body){
      var reqID = JSON.parse(body).headers['Request-Id']
      var sCode = res.statusCode
      console.log("Concurrent Response #"+reqID+" returned a "+sCode)
    })
  }
}

// PARALLEL REQUESTS
// pretty complex and for not much gain
parallizedRequests(5)
function parallizedRequests(count){
  var cluster = require('cluster')
  var count = count
  if(cluster.isMaster) {
    for(var i = 0; i < count; i++) {
      var worker = cluster.fork()
      worker.on('message', function(message) {
        console.log(message.data.result)
      })
    }
    var i = 0
    for(var wid in cluster.workers) {
      i++
      if (i > count) {return}
      cluster.workers[wid].send({
        type: 'request',
        data: {
          number: i
        }
      })
    }
  } else {
    process.on('message', function(message) {
      if(message.type === 'request') {
        sendRequest(message.data.number, function(res,body){
          var reqID = JSON.parse(body).headers['Request-Id']
          var sCode = res.statusCode
          process.send({
            data: {
              result: "Parallel Response #"+reqID+" returned a "+sCode
            }
          })
          process.exit(0)
        })
      }
    })
  }
}

Further Resources

// CONCURRENT REQUESTS
var http = require('http')
function concurrentRequests(){
for (var i = 0; i < 5; i++) {
var request = {
hostname: 'httpbin.org',
headers: {'request-id': i},
path: '/delay/.'+ Math.floor(Math.random() * (5 - 1 + 1)) + 1
}
http.get(request, (res) => {
var body = ''
res.on('data', function (chunk) {
body += chunk
})
res.on('end', function () {
console.log(JSON.parse(body).headers['Request-Id'])
})
}).end()
}
}
console.log("Running concurrent requests!")
concurrentRequests()
// PARALLEL REQUESTS
var http = require('http')
var cluster = require('cluster')
function getResponse(i, cb){
var request = {
hostname: 'httpbin.org',
headers: {'request-id': i},
path: '/delay/.'+ Math.floor(Math.random() * (5 - 1 + 1)) + 1
}
http.get(request, (res) => {
var body = ''
res.on('data', function (chunk) {
body += chunk
})
res.on('end', function () {
cb(JSON.parse(body).headers['Request-Id'])
})
}).end()
}
if(cluster.isMaster) {
console.log("Running concurrent requests!")
var numWorkers = require('os').cpus().length;
for(var i = 0; i < 5; i++) {
var worker = cluster.fork()
worker.on('message', function(message) {
console.log(message.data.result)
})
}
var count = 0
for(var wid in cluster.workers) {
count++
if (count > 5) {return}
cluster.workers[wid].send({
type: 'request',
data: {
number: count
}
})
}
} else {
process.on('message', function(message) {
if(message.type === 'request') {
getResponse(message.data.number, function(res){
process.send({
data: {
result: res
}
})
process.exit(0)
})
}
})
}
// SEQUENTIAL REQUESTS
var http = require('http')
function sequentialRequest(count){
if (count == undefined) {count = 0}
count++
if (count > 5) {return}
var request = {
hostname: 'httpbin.org',
headers: {'request-id': count},
path: '/delay/.'+ Math.floor(Math.random() * (5 - 1 + 1)) + 1
}
http.get(request, (res) => {
var body = ''
res.on('data', function (chunk) {
body += chunk
})
res.on('end', function () {
console.log(JSON.parse(body).headers['Request-Id'])
sequentialRequest(count)
})
}).end()
}
console.log("Running sequential requests!")
sequentialRequest()
@davmaz
Copy link

davmaz commented Oct 10, 2020

Thank you very much for illustrating exactly HOW to get code to execute sequentially! I have been pulling my hair out for hours trying to find a way to do this with Promise.all but it would never do each Promise.all sequentially. I'm surprised at how easy and tricky it is to actually get code to execute in an orderly manner in one particular case (like waiting for a JSON request to finish BEFORE continuing execution that uses the JSON).

@davmaz
Copy link

davmaz commented Oct 11, 2020

Great article! I just came to a major realization (thanks to you) about Javascript. It will always try to execute the current function and move on to the next. However, the trick of sorts, that you've shown here is that if the function is recursive AND the only one it has to execute, a program can be made truly sequential by having it step through recursive calls to the same function. It's so simple to realize once the "Aha" occurs.

@benjaminyakobi
Copy link

Hi! Thank you so much, perfect! Thank you :)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment