All this async code without decent locking primitives is leading to a rabbit hole of race conditions...
It doesn't matter that it's all single threaded if all your function calls may or may not block and run a bunch of other code in the meantime, mutating all kinds of state.
I feel like JavaScript developers of the 2020's are going to relearn the same things the C programmers of the 1990's learn, just a few levels of abstraction higher.
Data races are impossible in JavaScript because it's single-threaded. Race conditions are possible in any language that can do anything asynchronous, which is basically all of them. But the general benefit you get from JS being single-threaded is the fact that any given callback is transactional. No other code will ever come in and mutate state between two regular lines of JavaScript code. Achieving this is pretty much the whole point of locking mechanisms, so under normal circumstances they aren't necessary for JS.
That said, when you use async/await instead of promises or callbacks, things do change because two sequential lines of code are no longer two truly sequential instructions. You're muddying the most fundamental metaphor of code syntax. That's why I personally don't like async/await syntax, although I get why people want it.
Javascript devs, for all their flaws, understand async programming far, far better than the average C programmer.
Indeed, your assertion that we need "locking primitives" to counteract "race conditions" is evidence of that. Yes, JavaScript can have race conditions, but not multi-threaded race conditions that cause resource contention [0].
So what good would locking primitives be?
And as a solution to single-threaded race conditions, it's becoming more and more common to use pure functions and immutable data structures in Javascript. In the ReactJS world, it's practically standard to use immutable data structures.
Furthermore, JS devs know how to structure their code, either using nested callbacks or promises, or now, async/await, to avoid async data races. Anyone who programs primarily asynchronously understands these things.
So in summary, it's pretty arrogant to assume that JS devs are going to have to "re-learn" the same things that C programmers learned in the 1990s (also ignorant, because 1990s C was decidedly synchronous). The only devs I know that struggle with race conditions in Javascript are those who are coming from another language or paradigm and who fundamentally fail to understand asynchronous programming.
0. The only exception to this would be those NodeJS devs who use multiple processes or browser devs using web workers along with the ultra-new and not very well-supported shared (memory) resources, both of which are rare in the JS world because there's not much need. You can achieve adequate performance off of one thread for practically any IO-bound application unless you're operating at Facebook scale. And in those case when people are using multiple processes for CPU bound algorithms, they're almost always using them with async message queues anyway, which obviates the need for any locking primitives.
Edit: https://news.ycombinator.com/item?id=21065831 in this case, async/await can introduce a problem. But using an immutable data structure reference as would almost always eliminate this issue.
Also, in practice you're probably using a database whose library has transactions, so you'd "lock" the transaction in this way.
But OK, if you use async/await or generators along with mutability, then a locking primitive could be useful, I'll concede. Although in a single-threaded program, a boolean is just as good.
I am not sure how the parent comment has received so many upvotes. OP has some fundamental misunderstanding of mutexes and the purpose of async io.
Locking primitives are completely unnecessary in any single threaded program. Also mutexes in C or otherwise are way older than 1990. They go back to the 1950s.
When your program is single threaded, a simple boolean flag variable can act as a mutex, you don't need a mutex primitive for this.
Mutexes are for situations where flag variables can change state between your instructions to check and set a flag. This can only happen in multithreaded or interrupt driven code.
Infact, the entire purpose of async programming is to stop the use of mutexes and multiple threads to perform concurrent io, which is something that can be performed by a single thread. This is the foundational premise of nodejs.
I think JS devs usually use async/await for network calls and not really for computation or file access. Those type of applications are better served by other languages.
A deadlock in the problem space that JavaScript operates in would be a rarity I feel.
Despite other deficiencies, JavaScript devs are generally pretty good at writing async code, for the simple reason that you get burned suuuper quickly if you try to mutate state outside of an async context when doing things like adding click handlers. This is something most front-end devs learn pretty quickly since it's such a fundamental part of writing JavaScript.
> It doesn't matter that it's all single threaded if all your function calls may or may not block and run a bunch of other code in the meantime, mutating all kinds of state.
Can you elaborate a bit more on this? I'm unsure about how locking in a single-threaded environment would work. And how would it really differ from async/await or promises which can handle race conditions already?
One area where the lack-of locking primitives does worry me is the use of shared mutable memory. I've never used these techniques in JS, but if I remember right one can now do something like this:
1. Create a shared memory array
2. Spawn several workers which do work on shared arrays
3. Pass a reference to the array to each worker and let them run.
This seems like an area where we could start to see these problems start appearing. There's a module related to atomic operations [1] but there is no language-level enforcement of using it. There could be good reasons why this isn't worrying but I just don't happen to know that off hand.
Oh, you're going to love (hate) what I suspect will happen then:
> race conditions
I've gotten into multiple arguments with first-language-javascript devs who think race conditions can't happen in javascript "because it's not multithreaded". I'm thinking before they can accept this type of bug, it's going to get a new trendy name first, then all the old knowledge of "race conditions" continue to be ignored.
> All this async code without decent locking primitives is leading to a rabbit hole of race conditions...
I haven’t seen this happening. Maybe it’s because I tend to use Node for web apps too much, which don’t have much shared state within Node to begin with? That it’s single-threaded does matter immensely, though, because if you have shared state that can be updated synchronously, you just write the code and know that it runs as a unit.
It comes from functional programming concepts (namely monadic design - which makes Haskell the 'best imperative language') which has some nice algebraic properties. It's easy to reason about, and don't really need lock machanism above the abstraction if properly used.
Seems convenient. Though I've never thought of top-level async as the killer feature I've been waiting for out of V8. Unless I'm missing something about what this enables...
The "problem" with COMEFROM is that at the "target" location (the one control comes from) there is no in-source indication of the control flow transfer. So what looks like linear code turns out to have this unexpected detour to the location of the COMEFROM instruction. This hinders understandability of the code.
"await" in JS doesn't have that problem: there is an explicit control flow operation, in the form of resolving a promise, that eventually transfers control to the location of the "await" call. And even then, it's async from a run-to-completion perspective, so doesn't affect linear code, modulo other await calls. I guess the concern is that you could have linear code that calls a function, which does an "await" and you would effectively have a control flow detour that's hidden from view? In that sense, I guess this is sort of like COMEFROM... At least you have to explicitly opt in (via "async function", or async module) for it to be a problem.
Anyway, a better analogy from my point of view is that async/await is a very limited form of call-with-current-continuation or so. And with generator functions, JS already had that sort of, but without some of the nice ergonomics.
I think async/await probably makes more sense in a typed language, where a compiler can tell you when you're missing an await, or at least warn you about not dealing with potential side effects and error handling. For something like JavaScript, it'd make more sense to me to have the runtime always and implicitly await the result of async functions, and instead make developers explicitly say when they wish for the result to be async. For example, instead of:
const data = await fetch()
push(someData) // async, runs in the background
You would do:
const data = fetch() // Runtime detects promise and awaits result
async push(data) // the async keyword would return a promise and execute the push function asynchronously, allowing the next line to execute
In this fantasy world the "await" keyword would work anywhere and as you'd expect – awaiting the result of any promise:
const data = fetch() // implicitly await
await async push(data) // this would also be "synchronous" in that it suspends execution of subsequent code until `push` fulfills or rejects the promise, and so it'd have the same effect as implicit await
Point is you'd probably await that promise elsewhere, so you'd actually store away the return value of the `async` call and later on you'd `await`, or another example would be to await a block.
Promise rejections in implicit awaits would halt execution, just like a sync function throwing an error, so you wouldn't "miss" an error somewhere because runtimes swallow promise rejections. (Well, at least Node wised up eventually.)
This means there'd be no difference in function declaration between sync and async functions, it'd be determined by whether they return a promise or not which I think should be possible to statically determine by a JIT compiler in most cases, so not adding too much (if any) overhead.
Kind of a half baked thought, but point is I always felt the async/await thing was kind of backwards in JavaScript.
Promises are nice in JavaScript because they’re just a value with no magic. Anything can create them, not just async functions. Generic/higher-order functions and so on can get involved without needing to know the difference. A proposal to introduce magic at calls sounds really awful, sorry.
Making await implicit would make it difficult to manage parallel promises. You would either have to make an exception for Promise.all or add new syntax. It's also not unheard of to have hanging promises (fire and forget).
Types make it easier to catch mistakes early in general. Typescript has a compiler rule 'no-hanging-promises' to help avoid forgetting to await.
It's a stage 3 proposal[1], which according to the TC39 process[2] means "the solution is complete and no further work is possible without implementation experience, significant usage and external feedback."
In other words, it's all but standardized. Barring significant blockers coming from actual implementation experience this will most likely be ratified.
[+] [-] londons_explore|6 years ago|reply
It doesn't matter that it's all single threaded if all your function calls may or may not block and run a bunch of other code in the meantime, mutating all kinds of state.
I feel like JavaScript developers of the 2020's are going to relearn the same things the C programmers of the 1990's learn, just a few levels of abstraction higher.
[+] [-] _bxg1|6 years ago|reply
That said, when you use async/await instead of promises or callbacks, things do change because two sequential lines of code are no longer two truly sequential instructions. You're muddying the most fundamental metaphor of code syntax. That's why I personally don't like async/await syntax, although I get why people want it.
[+] [-] jnbiche|6 years ago|reply
Indeed, your assertion that we need "locking primitives" to counteract "race conditions" is evidence of that. Yes, JavaScript can have race conditions, but not multi-threaded race conditions that cause resource contention [0].
So what good would locking primitives be?
And as a solution to single-threaded race conditions, it's becoming more and more common to use pure functions and immutable data structures in Javascript. In the ReactJS world, it's practically standard to use immutable data structures.
Furthermore, JS devs know how to structure their code, either using nested callbacks or promises, or now, async/await, to avoid async data races. Anyone who programs primarily asynchronously understands these things.
So in summary, it's pretty arrogant to assume that JS devs are going to have to "re-learn" the same things that C programmers learned in the 1990s (also ignorant, because 1990s C was decidedly synchronous). The only devs I know that struggle with race conditions in Javascript are those who are coming from another language or paradigm and who fundamentally fail to understand asynchronous programming.
0. The only exception to this would be those NodeJS devs who use multiple processes or browser devs using web workers along with the ultra-new and not very well-supported shared (memory) resources, both of which are rare in the JS world because there's not much need. You can achieve adequate performance off of one thread for practically any IO-bound application unless you're operating at Facebook scale. And in those case when people are using multiple processes for CPU bound algorithms, they're almost always using them with async message queues anyway, which obviates the need for any locking primitives.
Edit: https://news.ycombinator.com/item?id=21065831 in this case, async/await can introduce a problem. But using an immutable data structure reference as would almost always eliminate this issue.
Also, in practice you're probably using a database whose library has transactions, so you'd "lock" the transaction in this way.
But OK, if you use async/await or generators along with mutability, then a locking primitive could be useful, I'll concede. Although in a single-threaded program, a boolean is just as good.
[+] [-] random314|6 years ago|reply
Locking primitives are completely unnecessary in any single threaded program. Also mutexes in C or otherwise are way older than 1990. They go back to the 1950s.
When your program is single threaded, a simple boolean flag variable can act as a mutex, you don't need a mutex primitive for this. Mutexes are for situations where flag variables can change state between your instructions to check and set a flag. This can only happen in multithreaded or interrupt driven code.
Infact, the entire purpose of async programming is to stop the use of mutexes and multiple threads to perform concurrent io, which is something that can be performed by a single thread. This is the foundational premise of nodejs.
[+] [-] drevil-v2|6 years ago|reply
A deadlock in the problem space that JavaScript operates in would be a rarity I feel.
[+] [-] james-mcelwain|6 years ago|reply
[+] [-] rictic|6 years ago|reply
Dealing with asynchrony has been the an issue in JavaScript from the beginning, as the fundamental model is event driven.
There are a number of mechanisms for coordinating work, including mutexes (see Atomics.wait()), but for >99% of code, Promises are a better option.
[+] [-] anderskaseorg|6 years ago|reply
[+] [-] pseudoramble|6 years ago|reply
Can you elaborate a bit more on this? I'm unsure about how locking in a single-threaded environment would work. And how would it really differ from async/await or promises which can handle race conditions already?
One area where the lack-of locking primitives does worry me is the use of shared mutable memory. I've never used these techniques in JS, but if I remember right one can now do something like this:
1. Create a shared memory array
2. Spawn several workers which do work on shared arrays
3. Pass a reference to the array to each worker and let them run.
This seems like an area where we could start to see these problems start appearing. There's a module related to atomic operations [1] but there is no language-level enforcement of using it. There could be good reasons why this isn't worrying but I just don't happen to know that off hand.
[1]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
[+] [-] Izkata|6 years ago|reply
> race conditions
I've gotten into multiple arguments with first-language-javascript devs who think race conditions can't happen in javascript "because it's not multithreaded". I'm thinking before they can accept this type of bug, it's going to get a new trendy name first, then all the old knowledge of "race conditions" continue to be ignored.
[+] [-] minitech|6 years ago|reply
I haven’t seen this happening. Maybe it’s because I tend to use Node for web apps too much, which don’t have much shared state within Node to begin with? That it’s single-threaded does matter immensely, though, because if you have shared state that can be updated synchronously, you just write the code and know that it runs as a unit.
[+] [-] wayneftw|6 years ago|reply
[+] [-] tlrobinson|6 years ago|reply
You could even have a method decorator (https://github.com/tc39/proposal-decorators) that emulates Java's "synchronized" keyword.
[+] [-] duxup|6 years ago|reply
[+] [-] masklinn|6 years ago|reply
That was an issue long before async/await was a thing.
[+] [-] colordrops|6 years ago|reply
[+] [-] namelosw|6 years ago|reply
It comes from functional programming concepts (namely monadic design - which makes Haskell the 'best imperative language') which has some nice algebraic properties. It's easy to reason about, and don't really need lock machanism above the abstraction if properly used.
[+] [-] pjmlp|6 years ago|reply
Don't assume all programming languages have the same design flaws as C.
[+] [-] winrid|6 years ago|reply
[+] [-] lcfcjs2|6 years ago|reply
[deleted]
[+] [-] trust07007707|6 years ago|reply
[+] [-] miguelmota|6 years ago|reply
[+] [-] megous|6 years ago|reply
Instead of:
I'll be maybe writing: Hrm?[+] [-] nilkanthjp|6 years ago|reply
[+] [-] paulddraper|6 years ago|reply
It not only works for the main entrypoint module, but all other modules as well.
Though I can say I've ever encountered a reason to have that....
[+] [-] danappelxx|6 years ago|reply
[+] [-] bandrami|6 years ago|reply
[+] [-] bzbarsky|6 years ago|reply
The "problem" with COMEFROM is that at the "target" location (the one control comes from) there is no in-source indication of the control flow transfer. So what looks like linear code turns out to have this unexpected detour to the location of the COMEFROM instruction. This hinders understandability of the code.
"await" in JS doesn't have that problem: there is an explicit control flow operation, in the form of resolving a promise, that eventually transfers control to the location of the "await" call. And even then, it's async from a run-to-completion perspective, so doesn't affect linear code, modulo other await calls. I guess the concern is that you could have linear code that calls a function, which does an "await" and you would effectively have a control flow detour that's hidden from view? In that sense, I guess this is sort of like COMEFROM... At least you have to explicitly opt in (via "async function", or async module) for it to be a problem.
Anyway, a better analogy from my point of view is that async/await is a very limited form of call-with-current-continuation or so. And with generator functions, JS already had that sort of, but without some of the nice ergonomics.
[+] [-] nickitolas|6 years ago|reply
[+] [-] pjmlp|6 years ago|reply
[+] [-] mstade|6 years ago|reply
Promise rejections in implicit awaits would halt execution, just like a sync function throwing an error, so you wouldn't "miss" an error somewhere because runtimes swallow promise rejections. (Well, at least Node wised up eventually.)
This means there'd be no difference in function declaration between sync and async functions, it'd be determined by whether they return a promise or not which I think should be possible to statically determine by a JIT compiler in most cases, so not adding too much (if any) overhead.
Kind of a half baked thought, but point is I always felt the async/await thing was kind of backwards in JavaScript.
[+] [-] minitech|6 years ago|reply
[+] [-] laughinghan|6 years ago|reply
[+] [-] Scooty|6 years ago|reply
Types make it easier to catch mistakes early in general. Typescript has a compiler rule 'no-hanging-promises' to help avoid forgetting to await.
[+] [-] tedivm|6 years ago|reply
[+] [-] xtreak29|6 years ago|reply
python -m asyncio
Starts a new repl with the compiler flag to explore top level await in a repl
[+] [-] iikoolpp|6 years ago|reply
[+] [-] galaxyLogic|6 years ago|reply
https://gist.github.com/Rich-Harris/0b6f317657f5167663b493c7...
[+] [-] lacampbell|6 years ago|reply
[+] [-] mstade|6 years ago|reply
In other words, it's all but standardized. Barring significant blockers coming from actual implementation experience this will most likely be ratified.
[1]: https://github.com/tc39/proposal-top-level-await
[2]: https://tc39.es/process-document/
[+] [-] paulddraper|6 years ago|reply
It's a Stage 3 proposal https://github.com/tc39/proposal-top-level-await
[+] [-] unknown|6 years ago|reply
[deleted]
[+] [-] Footkerchief|6 years ago|reply
[+] [-] craftoman|6 years ago|reply
[+] [-] self_awareness|6 years ago|reply
[+] [-] 1wheel|6 years ago|reply
[+] [-] The_rationalist|6 years ago|reply
[+] [-] pitaj|6 years ago|reply
[+] [-] thatguyagain|6 years ago|reply
[+] [-] w56rjrtyu6ru|6 years ago|reply
[deleted]