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.
Granted, my practical experience is limited. I haven't really run into any of these kinds of situations outside of database work in Node, and there you've got the major benefit of transactions to do the locking for you, so the async JavaScript code doesn't have to particularly care and can yield whenever it wants.
For me, the real benefit of await / async so far has mostly just been about improving code flow. Promises were already an excellent solution to the async problem, but their syntax for all but the most trivial example is something only a mother could love. async/await makes the code structure suddenly not necessarily look like callback hell, and in a lot of cases it's much more compact and easier to read. It greatly improves the chances that when I come back to it a month later, I won't have to reach back into the past and slap myself for writing that monstrosity. :)
I've done a lot of async/await and I really can't think of any situations where this has been a concern for me. If you're mutating state in method calls without explicitly passing it around, that might be an issue but that's a deeper design issue IMHO.
A mutex really can only exist to serialize async code. If you want it serialized that likely means that code shouldn't be async in the first place.
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.
The reason we don't have a mutex in javascript is that there are better solutions to the problems it solves.
I don't really see how it's a mutex though, you're just returning the same promise to multiple requests. It's far simpler and doesn't use a locking construct.
For example:
``` let release; const acquire = new Promise(resolve => release = resolve);
(async () => {
await acquire;
// use here
release(); // now others can use it
})();
```This is a bit different because multiple people can await the lock here so it's like a "one time multicast mutex" making it more akin to condition variables.
That said - thinking about it as a mutex is a really backwards way in my opinion.
One should think in terms of a dataflow graph, where data-independent nodes can run in any order, or in parallel. One should explicitly think about ordering of effects, and be explicit about effects in general. (Hence the rise of popularity of FP.)
Within the context of the argument you are making, this is disingenuous. There's a big pile of transistors which determine whether it is safe to reorder those instructions.
const foo = async () {
const ice = await freeze(water);
const wCream = await whip(cream);
const base = await pour(liquor, ice);
const cocktail = await put(base, wCream);
return cocktail.serve();
}
In the above fragment, `freeze` and `whip` may run in any order or in parallel, you don't get to choose. Still `pour` never runs before `freeze` (though it can complete before `whip`), and `put` can only run last. This is because the above is syntactic sugar, and the dataflow graph gets encoded in the promises graph, with `.then` clauses giving an unequivocal dependency order where applicable.Same in CPU: two loads can run in either order or in parallel, but an ADD that takes the result of both of them will only run when they both complete.
That example is:
const foo = async () =>
freeze(water)
.then(ice => whip(cream)
.then(wCream => pour(liquor, ice)
.then(base => put(base, cream)
.then(cocktail => cocktail.serve()))));Yes - the benefits of pure functions in particular are increasingly evident.
I thought async/await was just syntactic sugar for promises?
If you were using promises or callbacks it would be the same, except that it won't appear like two sequential lines anymore.
[0] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
[1] https://developer.mozilla.org/en-US/docs/Web/JavaScript/Refe...
Yes but that doesn’t mean other lines of code can’t be executing while waiting. The waiting isn’t truly blocking the entire application. Plenty of other things can be happening.
On the flip side, there is a Javascript transpiler called nodent which directly converts async functions into the corresponding `Promise.then` calls, with no generators in sight. This transpiler actually generates smaller & faster output than the standard generator-based approaches, since the async/await semantics map more directly to promises.
In other words, async functions really are semantic sugar for promises, even if they have some similarities to coroutines / generators.
To try it yourself, just go to http://nodent.mailed.me.uk/ , check the "spec compliant" option, and look at the generated output.
Although I have found that babel does seem to transpile them differently to Promises
I've definitely needed more locking primitives and ordering but it's mostly for remote code.
There are also a ton of bugs that can happen with memory allocation.
For example if you do 50 async and they all come back and once and try to all allocate memory and then perform some action at the end you can have too many in flight and then use too much memory.
It's not really threaded but you can run into similar problems.
I've done 10+ years of core JVM threading with raw threads and softare transactional memory datastructures and CAS operations and my experience there helps a ton.
Async is definitely not just 'free'
How is this different from normal multithreading?
You can wedge a NodeJS interpreter by not yielding, but you also have control over when that yielding happens.
The same claim can be made for Visual Basic. This always sounds like a stronger guarantee than it actually is. In practice, the exact same mistakes get made irrespective of what the runtime provides.
Ultimately, people have to learn about race conditions in order to write correct code in a nontrivial system. Brushing the 'majority' of such cases under the carpet just makes it harder to educate them.
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.
async function deduct(amt) {
var balance = await getBalance();
if (balance >= amt)
return await setBalance(balance - amt);
}
One way to resolve this would be with a mutex to protect the balance during the critical section (which is async). What would you suggest instead?if balance is a javascript variable, access shouldn't be provided by async functions. In the case that mutex is a remote resource of some sort (file or network resource) a process local lock won't solve this for you either.
It seems to me that the lack of locks make forces software engineers to consider the nature of their data instead of just grabbing for the inappropriate os lock.
async function totalSize(fol) {
const files = await fol.getFiles();
let totalSize = 0;
await Promise.all(files.map(async file => {
totalSize += await file.getSize();
}));
// totalSize is now way too small
return totalSize;
} async function totalSize(fol) {
const files = await fol.getFiles();
const sizes = await Promise.all(files.map(file => file.getSize()));
return sizes.reduce((acc, size) => acc + size);
}After sharing it at work, someone pointed out that it isn't technically a race condition. The problem isn't caused by operations happening in an unpredictable order. It's unlikely that any of the promises are already resolved, so the lhs is always evaluated first. It's just that the programmer is surprised by the order of operations.
The takeaway is unsurprising: that `await` should be a signal to make one think carefully about state change; having `await` right after `+=` should be a big signal.
But the fact that an unwary programmer can actually be tripped up is interesting, despite some other comments on this article claiming such trip-ups are inevitable.
But do you mean that JS 1. will read `totalSize` then 2. do the asynchronous call, then 3. add and set? Seems like it's ambiguous and JS could just as easily read `totalSize` after the call, and all would be OK.
Or is the ordering specified?
Thanks for this clever example!
totalSize += await getSize()
becomes totalSize = totalSize + await getSize()
So all the map callbacks run one by one, read totalSize as 0, and then suspend waiting for getSize(). Each one then resolves and assigns totalSize to be 0 + the size.The race is what order the getSize() calls return in since only the last one will control the return value. Otherwise the issue isn't a race but just a logical ordering bug.
(This isn't super different than doing array[two()] = one() since two will actually run first, so ex. array[i += 1] = i will modify i before assigning the value.)
Correct would be to change the body of the map to:
const fileSize = await file.getSize();
totalSize += fileSize;
or the whole function to: async function totalSize(fol) {
const files = await fol.getFiles();
const sizes = await Promise.all(files.map(file => file.getSize()));
return sizes.reduce((totalSize, size) => totalSize + size, 0);
} totalSize = await file.getSize() + totalSize;
Would it be fine then?That said, if new developers just blindly use async/await without understanding what's going on ... well that's another problem.
The field is filled with foot-guns, for some definition of guns (and for some definition of foot).
The problem with C, or better the problem with pre-emptive multitasking in general, is that even relatively knowledgeable developers were constantly hitting subtle issues with the memory model. Consider the good old trap of efficiently lazy-initializing a Singleton in Java: https://en.m.wikipedia.org/wiki/Double-checked_locking#Usage...
If you are handling data locally e.g in a file then you would ideally rely on OS write lock and only store the deltas and never change state. You could then calculate the balance from the log of deductions and insertions.
FYI this problem has not anything to do with async or JS.
async function changeBalance(account, amt) {
/*
* BEGIN;
*
* UPDATE accounts
* SET balance = balance - amt
* WHERE id = ${id};
*
* COMMIT;
*/
}
Of course, this is just a mutex in database transaction clothing :)On the front-end, I'd do a call to the server. On the server, I'd use a database transaction for that.
But I get your point. The way I deal with race condition in redux is to have a mutex. I.e. While something is being fetched, any call to this command is either ignored or queued up.
I use redux-saga for the "race-condition" and general flow, and call the various async functions from within sagas.
async function deduct(amt) {
var balance = await getBalance();
if (balance >= amt)
await setBalance(balance - amt);
var newbalance = await getBalance();
if (newbalance != (balance - amt)) {
await setBalance(balance + amt);
// tell the user the transaction failed...
}
} INSERT INTO CashFlow
(date, income, expense)
VALUES
(:date, :income, 0)
Get balance: SELECT sum(income)-sum(expense) AS balance
FROM CashFlow
What modern js still has to reinvent is in-client synchronizing storage which naturally resolves who waits on what (if at all). This is partially simulated by reactjs now by trading in a developer’s comfort at near zero price.All this “await/promise has clear semantics and we think async” is pointless hope because code should never race with itself. Data should, and the storage must be there to kill associated problems once and forever.
>What modern js still has to reinvent is in-client synchronizing storage which naturally resolves who waits on what (if at all)
I don't understand what this means but it makes me curious. What would be an example of this in one of the languages that have already "reinvented" it?
>it comes with a number of performance and memory/storage consumption implications.
Instead of selecting sum(), CREATE TRIGGER then and update a singleton on insert. You can even create a view with N recent transactions and catch inserts into it to maintain that N, if you don’t want a full history.
async function deduct(amt) {
await updateBalance(balance => {
if (balance >= amt) {
return balance - amt;
} else {
throw new Error("not enough balance");
})
}You can just use a busy flag. I am not familiar with JS, so this is approximate syntax.
while (busy) { await busyIsFalse } busy = true
Critical Section
busy = false
notify busyIsFalse
Simple boolean flags will work
The standard pattern is to use database row locking or calling a stored procedure that performs locking inside. Backend developers typically don't like the second solution but it provides a kind of API. Not to be overlooked if there are multiple services accessing it, especially in a polyglot environment.
In no particular order:
1. Pizza company debits by balance by $5
2. I withdraw $5
Assuming both those operations can yield due to e.g. async requests and that they can be resumed at any point, I don't know which order they will yield or resume in.Consider this interleaving:
1.1. Get balance, I have $5
2.1. Get balance, I have $5
1.2. Set balance to $0
2.2. Set balance to $0 (but there are total debits of $10!)
With locking: 1.0. Acquire account lock
1.1. Get balance, I have $5
2.1. Get balance: wait on lock
1.2. Set balance to $0
1.3. Release account lock
2.1. Get balance, I have $0
2.2. Can't set balance, will overdraw!
With regards to resource contention, anything that causes a waiter graph cycle can cause deadlocking, regardless of single- or multi-threading. I can't think of a compelling example here, but nobody expects to have deadlocks yet they still happen :)Maybe I'll learn something here, but can you explain how an async runtime with no locking primitives like JS could cause a waiter graph cycle?
A transfer funds method that does: 1. Lock account A 2. Get balance and check 3. Lock account B 4. Debit account A 5. Credit account B
If the two accounts transfer to each other at the same time, you can get: 1. A->B: Lock account A 2. A->B: Get balance and check 3. B->A: Lock account B 4. B->A: Get balance and check 5. A->B: Lock account B -- deadlock
One solution is to always acquire locks in the same order and release all of them when you fail to obtain one. You can do this in the transfer example by sorting by account ID (so always lock A before B).
Let's say that balance is in a database or behind a REST api. An os-lock won't solve the problems of other processes trying to update that same resource.
While I agree that in the general practice in JavaScript programming stops you from this kind of footgun, you can still be caught off-guard if you end up thinking that this programming style is immune to this kind of problem: locking is a big hammer with its own problems, but it will never cause data incorrectness.
Yes, that's why I referenced passing around a reference to an immutable data structure, which is fairly common in JS.
But to answer your question, in functional programming (also frequently ReactJS, if you use libraries like Redux), you don't mutate the data structure. You create a new data structure based off the old one.
Am I misunderstanding your question? Just because you use an immutable data structure doesn't mean it can't be updated.
And does gmail not work well for you?
Most of us are only using JavaScript because that's the only way to build browser applications (or compile-to-JS languages, which still require an understanding of JS or you'll run into problems).
In any case, I don't particularly like JS, so your dig fell short. But JS is necessary for many of us.
The javascript apocalypse is inevitable, we're just yet to reach the tipping point where this hard-requirement to use JS for web/browser applications is no longer there.
There was definitely threading in C in the 1990's, and threads are asynchronous by nature. There's a whole class of synchronization bugs that C and C++ developers had to learn to deal with, and while JavaScript developers get to avoid some by the nature of there being a single thread, that doesn't necessarily exempt them from all of them.
Indeed, nginx caused big waves due to its superior IO performance as the first popular async http server released in 2004.
But correct me if you have a different understanding of the term.
Lighttpd was released before nginx and was wildly popular for a good while. Not to mention other servers like AOLserver in the 90s.
The use of non blocking sockets was nothing new, used heavily in C code throughout the 90s and is the basis of asynchronous processing (refer to Stevens Unix network programming). You should also read documents by John Ousterhout written in the 90s about this topic.
Now just about every major GUI library was targeted in C or Pascal and used an event driven callback model. You can refer to the Windows API, the modern Mac carbon API which was developed in the 80s at Next and the older Mac SDK. The Windows SDK allows event driven programming for UI and IO. tcl/tk. Just about anything on top of X (ie Motif, GTK). The list will go on and on.
Do some research before making bold claims about C programmers not understanding asynchronous programming and callbacks.
Also I think the other reply is correct in asserting that your definition of async is making distinctions without differences.
Eg from 1995: https://web.stanford.edu/~ouster/cgi-bin/papers/threads.pdf Note that event driven programming was nothing new in 1995.. it was the basis for the Windows api designed years prior after all, but this was around the time that multithreading was pushed for everything.
Heck fibers: https://docs.microsoft.com/en-us/windows/win32/procthread/fi... have existed in the Windows api since at least 95.
If I have a main program, and I fork a child to handle a task, and use shared memory to communicate, how is that any different than Javascript executing and and async call that sets or returns a value? The Javascript runtime has the same behavior as the OS scheduler in this, and if there's a single processor, it necessarily will only execute one instruction at a time. There's still plenty of pitfalls to worry about and that's why locks were (and are) useful, and why they are included with OS thread implementations (which are really just a nice API on forks and shared memory often dealt with by the OS for additional benefit).
The lock in this case could be as simple as disabling the button as soon as the user clicks on it, but I've also find Promise-based locking functions to be very useful in solving these kind of problems.
I wouldn't call these "primitives" though - you don't need a language construct for it. It's easy enough to build the locks as functions working with Promises.
unless you were programming in... any GUI toolkit ever except the most toy ones ? even win 3.1 GUI primitives were async
I was thinking about C network programming, which despite what people are saying on here, was not event driven in the 1990s (I was there).
You know that Javascript is a C(++) program? That when you use TCP, the protocol is in C? The ethernet driver is written in C? The OS scheduler is written in C? That you're programming in a little sandbox, and all the concurrency around you in managed in C?
There has never been anything synchronous about C.
JavaScript is not implementation-defined. There's a spec, and there are interpreters in a number of different languages. Sure, most common JS interpreters/JIT and otherwise, are in C++, but that says nothing about whether or not they use an event-driven style underneath to program the interpreter.
And regarding C being synchronous, I'm talking about programming models, not underlying architecture of the computer or OS. If everything is programmed in async as you seem to suggest here, then we do we bother distinguishing the two? Why do most books on networking programming have a separate chapter devoted to async or event-driven programming? Why does the Unix socket API have `socket.setblocking(0)`?
But I suspect you know damn well what I'm talking about.
I've got to get off of social media.
In Javascript you have a little window into this through whatever the layer below provided you. So Javascript by definition has a (small) subset of the concurrency you can get in a systems language.
And no I'm not entirely sure what you're talking about. It sounds like you learned concurrency in Javascript, and define concurrency in terms of Javascript primitives. But that's merely a guess on my part.
Not the junior ones, they don't. I understand the need to want to talk about this advanced topic with only perfect and good developers, but this is almost never the case in concrete scenarios. Developers forget, developers share code with other developers and the consequent spaghetti mess is hard to reason about 100%, developers make mistakes, developers are sometimes yet to learn something, developers miss small bugs, new code deals with library code that might have an async bug, etc.
Sequential flow is much easier to reason about and control for, and if you ask for my opinion, we should only use async features if the benefits of their usage far outweigh the potential complexity and headache that they introduce if you don't "use them the right way".
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.
A deadlock in the problem space that JavaScript operates in would be a rarity I feel.
const fs = require('fs').promises
const path = require('path')
const filePath = path.join(__dirname, 'package.json')
const fileContents = await fs.readFile(filePath, 'utf8')
console.log(fileContents)I assume it would really do much the same thing as the await example. Is there any difference?
If they do the same thing, then what's the benefit of async/await? I guess it's that you can now write your own "fs.readFileSync()" or something like that in JavaScript when needed.
Though the few sync functions in the stdlib become less and less convenient, first when promises came out, then with async/await, and now with top-level await.
That said, the need for locks in JavaScript is much less than in a typical multi-threaded language, so it's a lot easier to keep track of and avoid deadlocks, if you're even using locks at all.
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.
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...
function transfer(amount, acct1, acct2) {
var current_balance = await acc1.balance()
if current_balance > amount {
await acct1.sub(amount)
await acct2.add(amount)
}
}
Now what happens if you get multiple calls to transfer? What you want is probably something like: function transfer(amount, acct1, acct2) {
await acct1.Lock()
....
await acct1.UnLock()
}If anything getting this right on the front-end is often extra problematic because of the high cost of round-tripping state to the server.
It's dangerous to assume that just because JS has a single main thread that you don't need to think about sequencing of operations and locking.
> All this async code without decent locking primitives is leading to a rabbit hole of race conditions...
No, it's not. Here's a lock:
let lock = Promise.resolve()
// ...
lock = lock.then(() => { /* critical section */ })
For the rare instances where more complex async flow control is required, there are a bunch of libraries for that (e.g. async[0], p-queue[1], etc.).To lay out one example as clearly as I can: suppose you had two calls to transfer with the same args.
* The 2nd call to `await acc1.balance()` was very quick beating the 1st call
* The `current_balance` is greater than `amount` and so it runs `await acct1.sub(amount)`
* Before that can finish, the 1st call to `balance()` returns with the old balance before subtraction begins.
* Now another transfer is initiated with the incorrect amount.
If `amount` is larger than `current balance` you've got an issue on your hand.
I believe one could implement a mechanism around this using a set of booleans/ints for each account and managing the call to `transfer` with each one of those. But that's the point - we could have primitives to do that.
By the way, there is a particular issue with this specific example. The `transfer` function signature must be marked as `async`. You would need to add this to make it run properly.
> 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.
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.
Of course you don’t need to use the existence of a Promise as your Boolean in this case. You can simply use the Boolean state in addition to the Promise because your code is not going to yield while atomically setting one Boolean variable.
let lock = Promise.resolve()
const wait = (callback) => lock = lock.then(() => callback());
Until the callback resolves the lock is held, so you can do: wait(async () => {
await step1();
await step2();
// release the lock.
});Also, nobody said that we need callback error propagation in a priority queue to act the same exact way that callback error propagation works in a straight promise chain... but if you just want to reject all waiters in case of an error, you can do that.
Take a look at sindresorhus' p-queue project - https://github.com/sindresorhus/p-queue - which is about 40 lines of code that take care of queueing exactly the way you want. And it's easy to modify if you want to reject the whole queue in case of an error, which is something that I've done myself in the past. You can see them discussing the possibility of that feature right here - https://github.com/sindresorhus/p-queue/issues/29
Long story short: Your problem is with ordering, not locking.
> Long story short: Your problem is with ordering, not locking.
Look up the definition of a concurrency lock.
You don’t really need what you call locking in a single-threaded world. You just need a state machine.
There simply is no concurrency in JS though, so there is no need for locking. There is no way that you would ever "unblock all coroutines waiting for the promise" because you just can't run more than 1 coroutine at a time. So your problem is with ordering, not concurrency and locks are a solution for concurrency.
And the simplest state machine is a boolean variable.
Anyway, I guess if you still disagree then you can go and ask the people who build JS engines why they don't want to add locks. Maybe ask the v8 team - they add stuff that isn't in the spec all the time and they haven't seen a big need for this, so they must know the answer...
> There simply is no concurrency in JS though,
There is no parallelism in JS (well there is with workers now), concurrency is not parallelism.
I will concede that I got the two terms confused, mostly because I haven’t had to think about this kind of stuff in years because there’s literally nothing I’ve ever had to do with JavaScript that’s required me to think about it.
But I’d really love to see some code where you can’t handle concurrently waiting coroutine priorities in a single threaded world. Concurrency without parallelism is not really a problem the way I see it.
When you are forced to yield inside of your critical section (a database call, a file write, whatever), as is common in NodeJS, you must acquire a lock that another coroutine can't run through.
> There's no way for multiple branches of code to access the same variable at the same time in JS.
This is 100% true and it’s the very reason why we do not need locks. There is no thing called a critical section in JavaScript because in JavaScript all of your code runs on a single thread and it’s all equal therefore none of it is more volatile than any other piece code. All that you need to manage asynchronous concurrency is good state management, not “locks”, because there is nothing to lock since there are not multiple threads asking any of your JS code for control of execution at the same exact time. It’s all scheduled for you onto your single thread.
With that, what you don’t get in JS of course, is shared-state parallelism.
So of course there are still critical sections--a critical section is a set of operations that must have uninterrupted access to a resource in order to maintain a desired level of consistency. If you have to yield (that is, `await` on a Promise or wait for the invocation of a callback), which sometimes you do have to do (i.e., you have to do IO, you've got a crunchy computation running in C++ outside of the JS loop, whatever), then you need to prevent access to that resource from whatever is being picked up next by the JS runtime while the first user is waiting for that IO to finish or that computation to come back.
To that end, locks are that "good state management" to which you refer. Most of the time one can use a mutex--the `async-lock` library is common--but I've even encountered cases where resource management must occur in structured forms and it was necessary to implement a read-write lock, where multiple readers can operate in tandem but all readers must close out before a writer can take control.
The basics don't change when your multitasking is cooperative rather than preemptive--all the same stuff still applies. This is not a controversial set of assertions. I don't understand at all where you're coming from.
This conversation was talking about access to variables and data within Javascript code itself. Any language is open to data races when dealing with external systems. I can see why having a `lock` construct can be helpful here, but it seems extraneous when the actual language doesn't support concurrent access and simple packages already exist. The 'async-lock' is just a wrapper around a few variables, and only works precisely because JS is already single-threaded and serialized.
There simply is no concurrency in JavaScript, so lock primitives are not solving any problem that JavaScript has.
Can you give an example of some code where you think locks would help you?
Regardless, if you were to have need of a lock, which was the basis of this chain of comments unless I'm missremembering, a read-write lock could improve performance, regardless of things being singlel threaded, precisely because of things being async (If you have N async tasks all read-only locking an rwlock, they can go on to fire async requests, which will run concurrently, despite the execution being single threaded).
You could even have a method decorator (https://github.com/tc39/proposal-decorators) that emulates Java's "synchronized" keyword.
Take a look at a Lisp, APL or a derivative, or Ada for examples of languages designed by people who knew what they were doing. Lisp grew in universities under the direction of hackers and there are several standard dialects now. APL was designed by a mathematician originally as a teaching aid. Ada was designed in a contest by the US DoD after spending a long while collecting requirements and it was then fully specified before any implementation work was done.
The Great Old Ones did not have access to any lost mystical knowledge. The Lisp inventors hadn't even sorted out variable scoping leading many Lisps to have dynamic scoping which is now widely agreed to be the wrong default. Also, the Lisp-1 versus Lisp-2 split. These were clearly people that were figuring it out as they went along, which they would be the first to admit.
> standard dialects
An oxymoron if there ever was one. :)
> APL was designed by a mathematician originally as a teaching aid.
...and it's virtually dead. There are many interesting ideas in there but it doesn't seem like Iverson's ideas about what is a desirable notation for thought resonated with any significant fraction of people. There were many years where APL was a standard part of a CS degree. With that much forced contact, you would expect it to have stuck around if it had so much going for it. (See: Pascal and BASIC, which punched above their weight in large part because of early introduction in schools.)
> Ada was designed in a contest by the US DoD after spending a long while collecting requirements and it was then fully specified before any implementation work was done.
Ada also has a lot of interesting ideas but... outside of the DoD, it's not exactly doing well either. It turns out that spending several years writing an enormous specification without regard to implementation leads to... unbelievably complex, expensive implementations that take years to reach the market. Who could have predicted that?
The economic cost of implementing the language is a salient part of its fitness. It doesn't matter how nice the language is if you can't actually use it because you don't have a working compiler.
C++ is only bad to the degree that you can completely wish away path dependence. (Hint: You can't.) There were many languages better than C++ designed at the time, but they didn't take off because they lacked the features we hate in C++ today. Those features existed as a path to lead the existing C programmers to C++. Meanwhile, other purer languages floated off in space, beautiful but unreachable by mortals.
JavaScript was just a sad rush job that we're all unfortunately stuck with. I agree with you on PHP.
They at least had the advantage of not being forced to design a language for a failed startup, to be easy to learn where that's defined as resembling C, and other asinine considerations people continue to take into account nowadays.
These were clearly people that were figuring it out as they went along, which they would be the firt to admit
I'm not claiming otherwise, but the birth of Lisp was a paper written by an AI researcher that had good thought poured into it, not something meant to look like C and be good enough for whatever its purpose was on a strict time schedule.
...and it's virtually dead.
Why would that matter? Why does every language need to appeal to many people? Why is that a desirable quality? There's still plenty of APL being written. I even have an article concerning this: http://verisimilitudes.net/2019-08-08
Who could have predicted that?
Ada was designed to be implemented efficiently and understood easily. It's harder to write a good C or C++ compiler than it would be to write a good Ada compiler; there's simply fewer people doing this and GNAT is popular. The only compiler I'm aware of for C that anyone actually uses, sans the behemoths of GCC and Clang/LLVM, is tcc, and almost no one uses that. There's more Common Lisp implementations that are actually used than there are for C and the only reason you'd see more C compilers for Ada is because it's a smaller language with a standard that lets the implementor get away with doing so little at the cost of every program written in the language and because the standard for a C compiler is so low that it's expected to have bugs.
The economic cost of implementing the language is a salient part of its fitness.
When you start judging something in computing by its fitness, it reminds me of a virus, and that reminds me of The UNIX-HATERS Handbook and its description of C and UNIX.
C++ is only bad to the degree that you can completely wish away path dependence.
I don't know what you mean by this. Common Lisp doesn't care how code is loaded and Ada has a nice with and use system.
I do know C++ is a gargantuan language where the very grammar is ambiguous and is parsed differently by different compilers and I'm also aware it has little in static analysis due to this. Debugging Common lisp is simple due to its interactivity and Ada was designed to be statically-analyzed.
Part of why I mentioned APL, Lisp, and Ada is because I use all three of these languages. I could care less what the masses use. Do racecar drivers care about the people driving vans? Vans are more popular than racecars. Why should computing be different?
As an aside, I find it interesting how many negative Internet points I'm receiving relative to the amount of responses there are. My figuring is many of these people can't argue with what I'm writing.
Especially web development.
The tooling is pretty wild and crazy but I see a lot of tools that really do solve problems.
If they were really good languages, people would not only strongly advocate for them, they would give credibility to their advocacy by writing plenty of great programs in them, even on their own time, because they are very productive in these languages. They would also create best-in-class tooling to further their productivity even more.
What actually happens with these languages is that a certain breed of highly opinionated but highly unproductive programmer gets lost in them, on their quest for a level of aesthetics or elegance or correctness that nobody but themselves actually values. They don't build platforms for others to build on, they build pillars to prove a point.
In any case, I can still find examples of these platforms. Emacs is such a platform. For that matter, StumpWM is a platform of sorts. A nice quality of Lisp is making any program extensible with it trivially. As for Ada, governments use it for critical systems and I can't tell you the particulars of those. They use Ada because people will die if the software fails, so they're seeking quality, not quantity. You're advocating for quantity, not quality.
As for APL, you don't build platforms or such things in APL. If you don't see the beauty in APL, then that's your loss.
They don't build platforms for others to build on, they build pillars to prove a point.
First you claimed people don't build great programs in these languages, which I've shown is false, and now you're arguing that they build things, but not things someone can build on. You're conflating extensible software with quality software here.
What actually happens with these languages is that a certain breed of highly opinionated but highly unproductive programmer gets lost in them, on their quest for a level of aesthetics or elegance or correctness that nobody but themselves actually values.
That happens, less so with Ada, but why would that even be a bad thing? Is it wrong for someone to spend time working on what they view as an ideal and finished program, rather than just writing something that's already been done and requires little thought or effort on their part? It's infinitely harder, I think, to write novel programs than it is to simply implement some specification or write a copy or something else. That's what I do with my work. I don't see why you'd look down on that.
I'll let you've the last word, if you want it. I'm not interested in replying in this chain any further.
This is unsurprising.
> You're trying to tell me garbage such as PHP is one of the best languages there is, effectively.
I wouldn't go that far with PHP, it's only used in a certain (lucrative) niche and I can't think of anything great written in it.
Python is a good language that grew organically much in the way that i described.
> As for APL, you don't build platforms or such things in APL. If you don't see the beauty in APL, then that's your loss.
It really isn't a net loss, because I consider the strive for beauty in a program a fool's errand. I get to write useful things in less time by foregoing that ideal.
> In any case, I can still find examples of these platforms. Emacs is such a platform.
Let's suppose that Emacs is a great program. Most languages that are not completely irrelevant have "that one thing" for people to point to. That's not enough. Furthermore, Emacs consists of about 25% of "ugly" C code.
> For that matter, StumpWM is a platform of sorts.
StumpWM? You're quickly running out of Steam here.
> First you claimed people don't build great programs in these languages, which I've shown is false
That's not my claim. My claim is "plenty of great programs" and "best-in-class tooling". A language like Lisp excludes best-in-class tooling or good performance by virtue of its design, so all programs that are written in it have a disadvantage right off the bat.
Ada had some potential here, but it got stuck. I suppose it had something to do with too much of it being proprietary solutions.
> You're conflating extensible software with quality software here.
I don't mean "platform" in the sense of "program that I can extend". I'm talking about the ecosystem and tooling surrounding the language, its "commons", if you will.
> Is it wrong for someone to spend time working on what they view as an ideal and finished program, rather than just writing something that's already been done and requires little thought or effort on their part?
First of all, this is a false dichotomy. Secondly, a program that is ideal and finished but less useful is a poor trade-off. A program that is less ideal and unfinished but has better functionality as a result is, in my view, a better program. More importantly though is not just the one program, but all the programs that can or can not exist, based on the trade-offs you chose.
If you just want to create pieces of art that the very few to appreciate, that's your prerogative. If instead you focus on usefulness however, you can create actual wealth, for yourself and for others.
> It's infinitely harder, I think, to write novel programs...
It is indeed, but it's much harder still to also make them ideal.
> That's what I do with my work. I don't see why you'd look down on that.
I don't look down on it. I just see nothing to look up to.
That was an issue long before async/await was a thing.
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.
Don't assume all programming languages have the same design flaws as C.
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.