Is increment an atomic operation in JavaScript?
Asked Answered
P

5

27

Is increment an atomic operation in JavaScript? If one thread is accessing ++i; and at the same time another one starts to access the operation will there be any problems?

Pasteur answered 30/3, 2017 at 11:3 Comment(1)
JavaScript does not have threads. It does have workers (if you are in the right environment) (developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/…) that can pass communications trough a message bus.Cyma
A
38

In JavaScript, a function always runs to completion. That means if a function is running, then it will run completely; only after that, the other function will be called. So, there is no chance of interleaving between statements (but in case of Java it is different).

If you are confused with asynchronous execution, then always remember async means later not parallel. So, coming to your problem, the answer is, No you will not face any problem, it will be a total atomic operation.

Amandaamandi answered 30/3, 2017 at 11:14 Comment(3)
Do you have a source for this?Peripteral
Note that this is only true for synchronous functions. For example, the async x = 1; async function foo() { x += await 1; } will pause at the await before completing and finish the rest (the actual assignment) on a subsequent cycle. This can lead to unexpected results: jsbin.com/zufozat/1/edit?js,console Similarly, generator functions do not necessarily run to completion - a yield will pause their execution until a next value is requested when they'd run to the next yield etc.Leven
@Peripteral - The specification defines the run-to-completion and single active thread semantics, more in my answer to this question.Maximilianus
M
7

If one thread is accessing ++i; and at the same time another one starts to access the operation will there be any problems?

That won't happen with a simple variable like i, because JavaScript is defined such that there can only be one active thread (the agent's executing thread) in a realm at any given time. ("realm" - roughly speaking, a JavaScript global environment and the things within it, such as your variables.) So the issue simply doesn't arise with normal variables or object properties. Your function can't be interrupted during synchronous operation; JavaScript defines "run to completion" semantics: whenever a "job" (like triggering an event handler) is picked up from the job queue and executed, it runs to completion before any other job can be executed. (For an async function, the logic can only be suspended at await, return, or throw, not in the middle of a synchronous compound arithmetic operation. It can be in the middle of a compound arithmetic operation involving await. More about this below. Similarly, for generator functions, their logic is suspended at yield.)

The only place you'd have to worry about this is if you're using shared memory, where the actual memory is shared between realms and thus could indeed be accessed by multiple threads at the same time. But if you were doing that, you'd be dealing with a SharedArrayBuffer or a typed array using a SharedArrayBuffer, not a simple variable or property. But yes, if dealing with shared memory, you're exposed to all the "glorious" fun of CPU operation reordering, stale caches, and so on. That's part of why we have the Atomics object, including Atomics.add, which atomically adds a value to an element in a typed array using shared memory. (But beware naïve usage! After all, another thread could overwrite the value just after your add finishes, before you read it... Which is why Atomics.add returns the new value, which is what you should use instead.) Atomics provides the bare building blocks necessary to ensure safe access to shared memory. (More about this in Chapter 16 of my book JavaScript: The New Toys: "Shared Memory and Atomics".)

Note: This all applies to standard JavaScript engines that comply with the specification, such as those found in web browsers and Node.js. Non-standard JavaScript environments, such as the the scripting support for JavaScript built into the Java Virtual Machine, may (of course) define alternative non-standard semantics.


Re async functions: There is no multithreading involved in async functions. But the fact the logic of the function is suspended at an await can cause some surprising behavior. That said, the place it can occur is clearly flagged with the await.

I wouldn't worry about the details below unless you have to. But for those who do...

Consider:

let a = 1;

async function one() {
    return 1;
}

async function example() {
    console.log(`Adding 1 to a`);
    a += await one();
}

console.log(`Start, a = ${a}`);
Promise.all([
    example(),
    example(),
    example(),
])
.then(() => {
    console.log(`All done, a = ${a}`);
});

(Technically, we could just use a += await 1;, because await will wrap its operand in an implied Promise.resolve(x), but I thought it would be clearer to show an actual promise.)

That outputs:

Start, a = 1
Adding 1 to a
Adding 1 to a
Adding 1 to a
All done, a = 2

But wait, we added 1 to a three times, it should have ended up being 4, not 2?!?!

The key is in the await in this statement:

a += await one();

That's processed like this:

  1. Get the current value of a and set it aside; call it atemp.
  2. Call one and get its promise.
  3. Wait for the promise to settle and get the fulfillment value; let's call that value addend.
  4. Evaluate atemp + addend.
  5. Write the result to a.

Or in code:

/* 1 */ const atemp = a;
/* 2 */ const promise = one();
/* 3 */ const addend = await promise; // Logic is suspended here and resumed later
/* 4 */ const result = atemp + addend;
/* 5 */ a = result;

(You can find this detail in EvaluateStringOrNumericBinaryExpression in the spec.)

Where we used example, we called it three times without waiting for its promise to settle, so Step 1 was run three times, setting aside the value of a (1) three times. Then later, those saved values are used and a's value is overwritten.

Generator functions have a similar behavior at yield, where their logic is suspended and then resumed later.

Again, there was no multithreading involved (and run-to-completion is fully intact), it's just that when an async function's logic reaches an await, return (explicit or implicit), or throw, the function exits at that point and returns a promise. If that was because of an await, the function's logic will continue when the promise the function awaited settles, and will (in the normal course of things) eventually settle the promise the async fucntion returned. But each of those things will happen to completion, and on a single active thread.

Maximilianus answered 1/11, 2022 at 12:56 Comment(5)
stale caches - That's something that actually isn't a problem on real CPUs: OSes only run threads across core whose caches are coherent with each other. Effects like StoreLoad reordering are due to the order of each core's accesses to coherent cache, due to microarchitectural effects within each core such as the store buffer. preshing.com/20120710/… is a handy mental model. See also software.rajivprab.com/2018/04/29/…Squadron
Also, stale load values from non-atomic operations can happen due to compiler optimizations (keeping values in registers, which are thread-private, instead of reloading from cache). Other than that nitpick about the reasons why care is needed about synchronization and memory ordering, looks good. Re: Atomics.add and then reading a different value: that's why atomic ops return a value, which as you hint at you should use instead of reading the shared variable again separate from the atomic RMW. Thanks for explaining why JS has atomics when it normally doesn't need them.Squadron
@PeterCordes - Thanks. I have to admit when you get to that level, it's beyond my expertise. :-) I understood from Lars T. Hansen at Mozilla (co-author of the shared memory and Atomics proposals) that there could be stale values involved when he was helping me with Chapter 16, but I probably misunderstood something.Maximilianus
It may come down to how you define "stale", because stores can't instantly become visible to other cores; not until out-of-order exec is sure it's non-speculative, and only after getting exclusive ownership of the cache line (MESI M state.) But then the value isn't "stale", IMO, the store just hasn't become visible yet. With cores doing their loads early and stores late, there's some time window. (But stores see their own stores in program order, via store-forwarding from the store buffer, otherwise single-threaded code would break.)Squadron
Or some people say a compiler is "caching" values in CPU registers, which other people then misinterpret as talking about the actual "CPU cache" (like L1d / L2 cache). I think that's one major source of this common misconception about how CPUs work. To some degree it's not necessary to understand where memory-reordering effects come from (which is perhaps why misconceptions go uncorrected so often when people discuss correctness, especially in ISO C++), but understanding how CPUs and caches actually work is often helpful for understanding performance effects such as false sharing.Squadron
G
5

Javascript is single threaded, So you need to worry about deallocks or dirty read problems. Why doesn't JavaScript support multithreading?

Goodsized answered 30/3, 2017 at 11:12 Comment(1)
What about generator functions ? Surely you can exit a generator function before it have ended!Muddleheaded
V
0

Javascript does not support multithreading. It may have web workers, but your question would not apply to this case, as workers do not share variables.

Vary answered 30/3, 2017 at 11:8 Comment(0)
S
-3

Yes there will be a problem. Even if Javascript is single threaded, i++ is (read + modify + write) 3 steps operation, So when anyone is taking the i variable from asyncfunction, the other one can set the modified i to the variable area. In order to solve this issue, you can use atomic variables instead of regular variable. When one process take the number with read another process can change the number with write during the first one modifying. No matter this is single thread or multithread, all the things happen the same. All the things I said here are retated to asyncfunction on Node.js.

Shirleenshirlene answered 22/5, 2020 at 13:53 Comment(7)
JavaScript is single threaded, so how can any code "take" the variable i at the same time than other code is modifying it ? You need threads to access the same variable at the same time.Quezada
I'm not talking about the client side. In node.js, can you modify the global properties safely without using atomic variables?Shirleenshirlene
So you have two node.js processes accessing some shared memory? I didn't know JS could use shared memory of IPC, but if it can, then yes atomicity would become a concern.Squadron
I have one async function and there are two process inside due to await. After awaiting one process, another one can reach the same global property over standard function.Shirleenshirlene
Can you post a minimal reproducible example that shows non-atomic increments losing counts, like N increments producing a total change of less than N in the shared variable?Squadron
var i; async function a(){ i++}; a(); If i++ increment contains io process, the var value can be handled by another process inside the async function by node.js child thread during modifiying by the first process?Shirleenshirlene
@​Erdal76t - That code doesn't provide the MRE that @PeterCordes asked for. You'd have to demonstrate calls to a that result in values in i that show a non-atomic increment. But a always increments i effectively atomically, because of JavaScript's run-to-completion and single-active-thread semantics.Maximilianus

© 2022 - 2024 — McMap. All rights reserved.