1

Javascript is single threaded and - Node.js uses an asynchronous event-driven design pattern, which means that multiple actions are taken at the same time while executing a program.

With this in mind, I have a pseudo code:

myFunction() // main flow
var httpCallMade = false // a global variable

async myFunction() {
   const someData = await callDB() // LINE 1 network call
   renderMethod() // LINE 2 flow1
}

redisPubSubEventHandler() { // a method that is called from redis subscription asynchronously somewhere from a background task in the program
   renderMethod() // LINE 3 flow2
}

renderMethod(){ 
   if(!httpCallMade) {
      httpCallMade = true //set a global flag
      const res = makeHTTPCall() // an asynchronous network call. returns a promise.
   } // I want to ensure that this block is "synchronized" and is not acessible by flow1 and flow2 simultaneously!
}

myFunction() is called in the main thread - while redisPubSubEventHandler() is called asynchronously from a background task in the program. Both flows would end in calling renderMethod(). The idea is to ensure makeHTTPCall() (inside renderMethod) is only allowed to be called once

Is it guaranteed that renderMethod() would never be executed in parallel by LINE2 and LINE3 at the same time? My understanding is that as soon as renderMethod() is executed - event loop will not allow anything else to happen in server - which guarantees that it is only executed once at a given time (even if it had a network call without await).

Is this understanding correct?

If not, how do I make synchronize/lock entry to renderMethod?

Tintin
  • 2,853
  • 6
  • 42
  • 74
  • 1
    No, if these are called asynchronously by different event triggers, `rednerMethod()` could be called multiple times and there could be multiple http requests in flight at once. – Jason Goemaat Jul 19 '21 at 19:40
  • well I know renderMethod would be called twice. The question was whether they could be called in parallel! I edited the code to make this point - please could you see it and let me know - if the makeHTTPCall() would be called twice now? – Tintin Jul 19 '21 at 19:45
  • yes but the firs time could be LINE2 or LINE3 - there is no way to ensure, right? – Tintin Jul 19 '21 at 19:49
  • If this was a Java code, we would have a racing condition and I must synchronize or use locs on `renderMethod()`. In JS I don't think so. I am interested to know if my understanding of the execution flow is correct. – Tintin Jul 19 '21 at 19:50
  • The execution of JavaScript code is single threaded. So two lines of the same execution context can never run at the same time. Asynchronous does not mean multithreaded. – t.niese Jul 19 '21 at 19:51
  • How your code is now, `makeHttpCall()` will only ever be called a single time. Any future calls to `renderMethod()` will see the flag is true and not call `makeHttpCall()`. Is that what you want? – Jason Goemaat Jul 19 '21 at 19:51
  • I think one of your assumptions is correct. The function won't be executing in two separate threads on the computer at the same time. If you have a global variable 'a' and do 'a = a + 1' in that function, you will not have to worry about a synchronization issue and missing an increment. But I'm not sure that is meaningful with your sample code and I'm not sure what you are trying to accomplish or prevent. It looks like you could be getting events from two different sources. You should probably want to call `makeHttpCall()` for each one, right? – Jason Goemaat Jul 19 '21 at 19:56
  • 1
    Is `makeHTTPCall` a synchronous function? (This would be atypical in modern code). It would be better to ask this question with realistic, runnable code. – trincot Jul 19 '21 at 19:56
  • What you really want is some kind of debounce. So when any event comes in that would cause you to want to call `renderMethod()`, it will A) call `makeHttpCall()` if there is not already a call in progress and B) schedule a call to `makeHttpCall()` if there is already one in progress for after the current call completes. Does this sound right? – Jason Goemaat Jul 19 '21 at 19:58
  • @trincot no - its a async call without await – Tintin Jul 19 '21 at 19:58
  • Assuming `makeHTTPCall` is _async_, then set `httpCallMade = true` **before** calling `makeHTTPCall` (but inside the `if`). e.g.: `if (!httpCallMade) { httpCallMade = true; const res = makeHTTPCall(); }` – Wyck Jul 19 '21 at 20:00
  • @JasonGoemaat - exactly. I have dumbified the real situation. The real situation is little more complex. The idea is to make sure makeHTTPCall() is only called once for a given set of parameters. But again, I am not here to know the "solution" for this problem. I want to know if I am thinking right and understand the aysnc handling with respect to the concurrency. – Tintin Jul 19 '21 at 20:00
  • @Wyck - well I don't think that is really needed. As t.niese said in his answer - "renderMethod() is not marked async and has no await in it so no code interleaving can take place here. So between entering renderMethod and exiting renderMethod no other JavaScript code will be executed in the same context." – Tintin Jul 19 '21 at 20:04
  • You may also have intended `if (!httpCallInProgress) { httpCallInProgress = true; makeHTTPCall().then(() => { httpCallInProgress = false }); }` If it's really intended just to prevent starting a second one while the first one's in progress.. I'm not 100% sure what you're trying to accomplish. – Wyck Jul 19 '21 at 20:06
  • Deleted my answer for a moment because i just saw `// an asynchronous network call`. How can it be asynchronous if you don't have `await` or a callback? – t.niese Jul 19 '21 at 20:07
  • @t.niese lets say its simply returning a promise pre-ES6? – Tintin Jul 19 '21 at 20:08
  • After your edit, the `if` condition will never be true... :/ I repeat my call for realistic code. Now we have to guess what is async, what is not, when what is called,...etc – trincot Jul 19 '21 at 20:10
  • @trincot - yes sorry - corrected it. Why is this not realistic? Consider a scenario where I have to update a third party with http call whenever some condition is fulfilled. It can happen with a user-action (main thread) or it can happen due to a background job (redis pub-sub). No matter which flow invokes it, I don't want to call the external http end point twice with same information. – Tintin Jul 19 '21 at 20:15
  • With realistic, I mean with native methods or methods from a library which is mentioned, instead of pseudo code where the interpretation of what happens can vary. – trincot Jul 19 '21 at 20:18
  • What do you mean when you say _I want to ensure that this method is "synchronized" and is not called by flow1 and flow2 simultaneously!_ Do you mean that you want to ensure that the `makeHTTPCall` has fully resolved? In that case you either need to become async and await makeHTTPCall, or write `return makeHTTPCall()` to return its promise, or you need to write `makeHTTPCall().then( () { /* what to do next */ })` But you can't make an async function become synchronous. [Here's why](https://stackoverflow.com/questions/9121902/call-an-asynchronous-javascript-function-synchronously). – Wyck Jul 19 '21 at 20:20
  • @Wyck I am not sure if await or async are needed here. You are saying - with the code the way it is - interleaving of calls from LINE 1 and LINE 3 could happen inside `makeHTTPCall`? This is what i want to know. Why? for JS execution time - it is simply executing a block of code (the boolean value is native to JS and doesn't involve any network/io). This boolean value could never be updated at same time - i.e. there is NO race condition. And if there is no race-condition, the code - as is written should work as expected and the makeHTTPCall() wouldn't be called more than once. – Tintin Jul 19 '21 at 20:25
  • @t.niese - Your answer was so right on mark. Why did you remove it? – Tintin Jul 19 '21 at 20:27
  • I am saying if `makeHTTPCall` directly calls `renderMethod`, then it's possible to arrive at the beginning of the function via "reentrancy". If `makeHTTPCall` is _async_, and maybe has an event handler attached to it that calls `renderMethod`, then, the way you have it currently set up where `renderMethod` is **not** marked async, then there's no danger of having `renderMethod` be called while the other call to `renderMethod` is in progress because the method is not async. However the `renderMethod` **can** be called while the **httpCall** is in flight, so I'm not sure what you accomplish. – Wyck Jul 19 '21 at 20:31
  • @Wyck I know what you are saying. The thing is even if "reentrancy" happens, the boolean would prevent the execution. The simple answer I guess here is - this code is correct and would work with the expectation that makeHTTPCall can never be called twice here. Look at the answer by trincot. He caught the bulls eye. It was a fundamental question. I am not trying to get the solution for a specific problem here. – Tintin Jul 19 '21 at 20:34

4 Answers4

2

Javascript is single-threaded. Therefore, unless you are deliberately using threads (eg. worker_threads in node.js) no function in the current thread can be executed by two parallel threads at the same time.

This explains why javascript has no mutex or semaphore capability - because generally it is not needed (note: you can still have race conditions because asynchronous code may be executed in a sequence you did not expect).

There is a general confusion that asynchronous code means parallel code execution (multi-threaded). It can but most of the time when a system is labeled as asynchronous or non-blocking or event-oriented INSTEAD of multi-threaded it often means that the system is single-threaded.

In this case asynchronous means parallel WAIT. Not parallel code execution. Code is always executed sequentially - only, due to the ability of waiting in parallel you may not always know the sequence the code is executed in.

There are parts of javascript that execute in a separate thread. Modern browsers execute each tab and iframe in its own thread (but each tab or iframe are themselves single-threaded). But script cannot cross tabs, windows or iframes. So this is a non-issue. Script may access objects inside iframes but this is done via an API and the script itself cannot execute in the foreign iframe.

Node.js and some browsers also do DNS queries in a separate thread because there is no standardized cross-platform non-blocking API for DNS queries. But this is C code and not your javascript code. Your only interaction with this kind of multi-threading is when you pass a URL to fetch() or XMLHttpRequest().

Node.js also implement file I/O, zip compression and cryptographic functions in separate threads but again this is C code, not your javascript code. All results from these separate threads are returned to you asynchronously via the event loop so by the time your javascript code process the result we are back to executing sequentially in the main thread.

Finally both node.js and browsers have worker APIs (web workers for browsers and worker threads for node.js). However, both these API use message passing to transfer data (in node only a pointer is passed in the message thus the underlying memory is shared) and it still protects functions from having their variables overwritten by another thread.

In your code, both myFunction() and redisPubSubEventHandler() run in the main thread. It works like this:

  1. myFunction() is called, it returns immediately when it encounters the await.

  2. a bunch of functions are declared and compiled.

  3. we reach the end of your script:

       // I want to ensure that this method is "synchronized" and is not called by flow1 and flow2 simultaneously!
     }
     <----- we reach here
    
  4. now that we have reached the end of script we enter the event loop...

  5. either the callDB or the redis event completes, our process gets woken up

  6. the event loop figures out which handler to call based on what event happened

  7. either the await returns and call renderMethod() or redisPubSubEventHandler() gets executed and call renderMethod()

In either case both your renderMethod() calls will execute on the main thread. Thus it is impossible for renderMethod() to run in parallel.

It is possible for renderMethod() to be half executed and another call to renderMethod() happens IF it contains the await keyword. This is because the first call is suspended at the await allowing the interpreter to call renderMethod() again before the first call completes. But note that even in this case you are only in trouble if you have an await between if.. and httpCallMade = true.

slebetman
  • 109,858
  • 19
  • 140
  • 171
1

You need to differentiate between synchronous and asynchronous, and single- and multi-threaded.

JavaScript is single-threaded so no two lines of the same execution context can run at the same time.

But JavaScript allows asynchronous code execution (await/async), so the code in the execution context does not need to be in the order it appears in the code but that different parts of the code can be executed interleaved (not overlapped) - which could be called "running in parallel", even so, I think this is misleading.

event-driven design pattern, which means that multiple actions are taken at the same time while executing a program.

There are certain actions that can happen at the same time, like IO, multiprocessing (WebWorkers), but that is (with respect to JavaScript Code execution) not multi-threaded.

Is it guaranteed that renderMethod() would never be executed in parallel by LINE2 and LINE3 at the same time?

Depends on what you define as parallel at the same time. Parts of logic you describe in renderMethod() will (as you do the request asynchronously) run interleaved, so renderMethod(){ if(!httpCallMade) { could be executed multiple times before you get the response (not the Promise) back from makeHTTPCall but the code lines will never executed at the same time.

My understanding is that as soon as renderMethod() is executed - event loop will not allow anything else to happen in server - which guarantees that it is only executed once at a given time (even if it had a network call without await).

The problem here is, that you somehow need to get the data from your async response.

Therefore you either need to mark your function as async and use const res = await makeHTTPCall() this would allow code interleaving at the point of await. Or use .then(…) with a callback, which will be executed asynchronously at a later point (after you left the function)

But from the beginning of the function to the first await other the .then not interleaving could take place.

So your httpCallMade = true would prevent that another makeHTTPCall could take place, before the currently running is finished, under the assumption that you set httpCallMade to false only when the request is finished (in .then callback, or after the await)

// I want to ensure that this method is "synchronized" and is not called by flow1 and flow2 simultaneously!

As soon as a get a result in an asynchronous way, you can't go back to synchronous code execution. So you need to have a guard like httpCallMade to prevent that the logic described in renderMethod can run multiple times interleaved.

t.niese
  • 39,256
  • 9
  • 74
  • 101
  • yes, that is my understanding too. So in this case makeHTTPCall() could never end up being called more than once - right? – Tintin Jul 19 '21 at 20:02
  • @Tintin I finished updating the answer. – t.niese Jul 19 '21 at 20:39
  • @t.neise - question - what is "same execution context". Aren't two different HTTP Request Handlers in the same node process the "same execution context". It would be great if you could include clarity on this in your answer. – Tintin Jul 19 '21 at 20:40
  • 1
    @Tintin You can use `WebWorkers` (or `worker_threads` in node.js). Each worker and the main application have their own execution context (so this should be considered multiprocessing and not multithreading). The workers and the main application could run the same code but they don't share variables or memory (except if you use `SharedArrayBuffer`). And the workers could also do HTTP requests. So theoretically you could create two workers that run `makeHTTPCall` at the same time (Comparable to launching your application multiple times in parallel) – t.niese Jul 19 '21 at 20:46
  • sure, thats an explicit multi-processing, that the code owner must be aware of. Thanks. – Tintin Jul 19 '21 at 20:54
  • 1
    @Tintin yes absolutely. But saying that two lines of code can not run at the same time in general, is misleading because workers exist in modern js. But while those run at the same time it is fundamentally different from actual multithreading where variables are shared. So sure you can have certain race conditions with workers that also have with multithreading, but mostly only if you do IO. – t.niese Jul 19 '21 at 20:59
1

Your question really comes down to:

Given this code:

var flag = false;

function f() {
    if (!flag) {
        flag = true;
        console.log("hello");
    }
}

and considering that flag is not modified anywhere else, and many different, asynchronous events may call this function f...:

Can "hello" be printed twice?

The answer is no: if this runs on an ECMAScript compliant JS engine, then the call stack must be empty first before the next job is pulled from an event/job queue. Asynchronous tasks/reactions are pushed on an event queue. They don't execute before the currently executing JavaScript has run to completion, i.e. up until the call stack is empty. So they never interrupt running JavaScript code pre-emptively.

This is true even if these asynchronous tasks/events/jobs are scheduled by other threads, lower-level non-JS code,...etc. They all must wait their turn to be consumed by the JS engine. And this will happen one after the other.

For more information, see the ECMAScript specification on "Job". For instance 8.4 Jobs and Host Operations to Enqueue Jobs:

A Job is an abstract closure with no parameters that initiates an ECMAScript computation when no other ECMAScript computation is currently in progress.

[...]

  • Only one Job may be actively undergoing evaluation at any point in time.
  • Once evaluation of a Job starts, it must run to completion before evaluation of any other Job starts.

For example, promises generate such jobs -- See 25.6.1.3.2 Promise Resolve Functions:

When a promise resolve function is called with argument resolution, the following steps are taken:

  • [...]
  • Perform HostEnqueuePromiseJob(job.[[Job]], job.[[Realm]]).
trincot
  • 317,000
  • 35
  • 244
  • 286
  • Exactly the "conceptual insight" I was looking for. Especially the last paragraph. Would you have some specification link / documentation that formally guarantees this? Thanks a lot. – Tintin Jul 19 '21 at 20:31
  • 1
    I added some quotes to my answer. – trincot Jul 19 '21 at 20:40
0

It sounds like you want to do something like a 'debounce', where any event will cause makeHttpCall() execute, but it should only be executing once at a time, and should execute again after the last call if another event has occurred while it was executing. So like this:

  1. DB Call is made, and makeHttpCall() should execute
  2. While makeHttpCall() is executing, you get a redis pub/sub event that should execute makeHttpCall() again, but that is delayed because it is already executing
  3. Still before the first call is done, another DB call is made and requires makeHttpCall() to execute again. But even though you have received two events, you only need to have it called one time to update something with the most recent information you have.
  4. The first call to makeHttpCall() finishes, but since there have been two events, you need to make a call again.

const makeHttpCall = () => new Promise(resolve => {
  // resolve after 2 seconds
  setTimeout(resolve, 2000); 
});

// returns a function to call that will call your function
const createDebouncer = (fn) => {
  let eventCounter = 0;
  let inProgress = false;
    
  const execute = () => {
    if (inProgress) {
      eventCounter++;
      console.log('execute() called, but call is in progress.');
      console.log(`There are now ${eventCounter} events since last call.`);
      return;
    }
    
    console.log(`Executing...  There have been ${eventCounter} events.`);
    eventCounter = 0;
    inProgress = true;
    fn()
    .then(() => {
      console.log('async function call completed!');
      inProgress = false;
      if (eventCounter > 0) {
        // make another call if there are pending events since the last call
        execute();
      }
    });
  }
  
  return execute;
}

let debouncer = createDebouncer(makeHttpCall);

document.getElementById('buttonDoEvent').addEventListener('click', () => {
  debouncer();
});
<button id="buttonDoEvent">Do Event</button>
Jason Goemaat
  • 28,692
  • 15
  • 86
  • 113