How to make sure the SAME JavaScript function will NOT be called simultaneously by separate event?

Viewed 274

I have a multi-users chat web pages written in JS and socket.io.

Many users can send messages simultaneously, when I receive a message, I need to run a function that changes the innerHTML of a DIV.

However, suppose there are 1000 messages send to be simultaneously, I want to run THAT function in sequence, one time after another.

i.e. DO NOT run another call for that function UNTIL last call is completed. (And the GLOBAL variables inside that function will NOT be changed by another call of the SAME function, until the current call is completed)

e.g. (I write the following example by using timer instead of socket.io event since it is much simple to understand)

Below is the Javascript:

var gInteger = 0;
var gbolChanging = false;

function changeInteger (intChange) {
  if (!gbolChanging) {
    gbolChanging = true;
   gInteger += intChange;
    divInteger.innerHTML = gInteger;
    gbolChanging = false;
  }
}

var timer1 = setInterval(changeInteger, 1, 1);
var timer2 = setInterval(changeInteger, 1, -1);
<div id="divInteger">

</div>

Above is the HTML:

The expected result of the above html and js is:

You should see only 0 or 1

The actual result is:

You may see -1 and 2 as well if you run the program long enough. (i.e. both timers detects bolChanging = false at the same time)

2 Answers

Javascript is not multithreaded, therefore the same function cannot be called simultaneously by separate events. More generally, the same variable cannot be accessed by different instructions at the same time (in threaded environment a variable accessed by two threads may be duplicated in memory and synchronization issues occur).

As a proof, do a long synchrounous operation in an event handler and see that your interface will freeze until it's over (and no other events can be handled).

What you get is not the result of the function being called at the same time but the result of the order of the calls being non predictables.

Now you can still make sure the order is respected by the use of some kind of semaphores:

var gInteger = 0;
var gbolWaitingForPlusOne = true;
var gbolWaitingForMinusOne = false;
var divInteger = document.getElementById('divInteger');

function changeInteger(intChange) {
    if (
        (gbolWaitingForPlusOne && intChange !== 1) ||
        (gbolWaitingForMinusOne && intChange !== -1)
    ) {
        return;
    }
    gbolWaitingForPlusOne = !gbolWaitingForPlusOne;
    gbolWaitingForMinusOne = !gbolWaitingForMinusOne;
    gInteger += intChange;
    divInteger.innerHTML = gInteger;
    // if you don't log you wont see because it's too fast
    console.log(gInteger);
}

var timer1 = setInterval(changeInteger, 1, 1);
var timer2 = setInterval(changeInteger, 1, -1);
<div id="divInteger">0</div>

That still won't fix your issue thou because this way messages would be lost.

You will need to accumulate and reorder not according to the kind of operation (+1/-1) but by some kind of timestamp. In a messaging system, you would reorder the messages by their creation time, not their process time. If two ore more messages are created at the exact same time (which is well possible), you will have to choose yourself in which order to display them but there won't be any good reason to make one appear before another with the same creation date (this is actually what javascript did when doing your calls in the "wrong" order)

First of all, thank you remix23, I think I've found the answer to sequence the calls without missing calls by an Array:

var gintSumA = 0;
var gintSumB = 0;
var gintSumC = 0;
var gintSumAll = 0;
var gintToBeProcess = 0;
var timerWork, timerA, timerB, timerC;
var gintMax = 1000;
var gbolWorking = false;
var garrList = [];

function funStartAdd(strCaller, intNumber) {
  switch (strCaller) {
    case "A":
      if (gintSumA < gintMax) {
        garrList.push(intNumber);
        gintSumA += intNumber;
        gintToBeProcess += 1;
      } else {
        clearInterval(timerA);
      }
      break;
    case "B":
      if (gintSumB < gintMax) {
        garrList.push(intNumber);
        gintSumB += intNumber;
        gintToBeProcess += 1;
      } else {
        clearInterval(timerB);
      }
      break;
    case "C":
      if (gintSumC < gintMax) {
        garrList.push(intNumber);
        gintSumC += intNumber;
        gintToBeProcess += 1;
      } else {
        clearInterval(timerC);
      }
      break;
    default:
      // Do Nothing
  }
}
function funActualAdd(intNumber) {
  gintToBeProcess -= 1;
  gintSumAll += intNumber;
}
function funWork() {
  if (!gbolWorking) {
    gbolWorking = true;
    var intTemp = garrList[0];
    garrList.splice(0,1);
    funActualAdd(intTemp);
    
    if (gintSumA == gintMax &&
     gintSumB == gintMax &&
     gintSumC == gintMax &&
     gintToBeProcess == 0) {
    // Display Result
    divResult.innerHTML = "SumA = " + gintSumA + "<br/>" +
                   "SumB = " + gintSumB + "<br/>" +
                   "SumC = " + gintSumC + "<br/>" +
                   "SumAll = " + gintSumAll;
    clearInterval(funWork);
    }
    gbolWorking = false;
  }
}

timerA = setInterval(funStartAdd, 1, "A", 1);
timerB = setInterval(funStartAdd, 1, "B", 1);
timerC = setInterval(funStartAdd, 1, "C", 1);

timerWork = setInterval(funWork, 1);
<div id="divResult">

</div>

Related