It's implied (but sadly not stated) in the post that they asked for baseten's permission before conducting this research.
What's interesting to me as someone who has sold a lot of software to a lot of software companies is that many enterprise vendor agreements explicitly allow companies to pentest their vendors with advance notice and coordination. I don't think any of our clients ever exercised that clause; I expect it's going to be exercised a lot more going forward because it's so easy to do now.
I answered the quiz and it said "you must be a Javascript developer", which is true enough - that's probably my second-most-proficient language. In fact, I'm a Ruby developer partially because I hate the idea of async/await and I'm feeling very smug about my choice after reading this.
Some of these design decisions seem indefensible to me. For example, what the authors call "Suspension":
-> Static: Await points guaranteed to suspend -- JavaScript
-> Dynamic: No guarantees on awaiting tasks -- C# · Swift · Tokio · Smol · Asyncio · Trio
What is "await" if not a synonym for "suspend"?!?
async/await is one product of a long line of thought that says "threads are too hard for programmers to get right". Threads (really, shared memory) have real usability issues for developers, but once you grok the semantics (which largely map to the physical execution model in a CPU) that knowledge is transferrable across virtually all languages and runtimes.
> async/await is one product of a long line of thought that says "threads are too hard for programmers to get right".
Having used both threads and async/await and using them both in the same program, I don't see how async/await is supposed to make it easier to get right.
In my experience, async/await seems to be a solution to avoid running too many threads. In Javascript, because you could only have one thread in browsers; in other languages because thread per X is too many threads and queuing to a thread pool might not be desirable either.
Async/await always feels terrible to use though. Some other way to get thread like semantics without having to have OS threads for everything seems better (to me). Erlang processes, Java Loom Virtual Threads (which I haven't used), etc. If it avoids having all memory shared, even better.
In the 1990s, threads were programmed with extensive use of semaphores and threads arbitrarily running around shared data structures. This is a disastrous approach to threading, and I agree with pretty much every scathing condemnation written about it.
The problem is, the community collectively decided the problem was "threading" in general rather than "trying to have tons of threads running around shared data structures controlled via piles of simultaneously-held semaphores" specifically.
If you don't structure your threads on that basis, but instead default to something that looks more like actors and message passing, even if it isn't strictly speaking actors and message passing, the complexity comes down. Add some later elaborations like structured concurrency and a few other pre-canned design patterns for threading like a parallel map or worker pools being issued work items and it becomes merely something difficult rather than insane. When you program with threads sanely, it takes very little for async/await to actually be the substantially more complicated and difficult-to-understand choice when you have a workflow more interesting than "always await everything immediately" to implement, to say nothing of how nice it is to have things actually running on multiple cores simultaneously without having to carefully arrange for it.
Linux has 8MB thread stacks by default, Windows apparently has 1MB ones, and that space is not going anywhere. As long as people are worried about memory, they will need something lighter than threads. (Yes, golang managed to create dynamic stacks, but this required major support from compiler and so unlikely to appear in existing languages).
Also, I think that single-threaded programs, even with co-routines, are just so much nicer than multi-threaded ones. You write "index = last_index++;" and it Just Works (tm), no need to worry about locks or atomics or other thread access.
> Yes, golang managed to create dynamic stacks, but this required major support from compiler and so unlikely to appear in existing languages
That's true but I'm puzzled by the decision rationale. It's undeniably a major undertaking to add first class, fine-grained processes to a language and its runtime. But time invested there gets the multiplicative upside that all language users benefit from the investment. Instead, Async/Await transfers the complexity to users of the language, as TFA describes.
As an Erlang and now gleam developer, I'm continuously grateful for the BEAM's support for fine-grained processes (note these are VM processes, not OS level). If I want to do things in parallel, I spawn a new process to do it. Do I want that concurrency because of io latency or parallel computation? Doesn't matter. Processes handle both. If I want an actor - a long(ish) lived "object" that responds to messages sent to it - I spawn it as a process. If I want to communicate between processes, I send a message. That's the only choice. No shared memory so no semaphores, locks and whatnot.
I never have to think "hmm, should this function be sync or async?" and reason about the transitive implications through the entire call stack. I write functions to calculate values. If I want function A to be called after function B in program 1, I write them sequentially. If I want to run them concurrently in program 2, I spawn them in separate processes. Concurrency is a decision at the calling site, not when writing the function being called.
One concurrency primitive that meets all the needs. The reduction in cognitive load is palpable compared to Python (the other language I use regularly).
The usual reaction is "yeah but performance". I've never found this to be an issue in real life. Sure there are benchmarks that show C/Rust/C#/whatever is faster, often meaningfully so. In practice, for my needs: never been a problem.
I'm ever more grateful for the elegance and consistency of the BEAM concurrency model. From an ergonomic perspective, Async/Await feels like a poor abstraction by comparison.
That's not to say the BEAM (or its languages) is the final word in concurrency. The strong encapsulation boundaries from Structured Concurrency[0] would be a useful addition. Though even there, Erlang's supervisor hierarchies provide a a similar mechanism. Dataflow is another interesting area (many task-concurrent design questions are essentially dataflow problems).
Even without improvement though I'd still take Erlang's approach over Async/Await every day.
More people think they need a web framework that serves a million requests per second than actually do.
More people think they need the latest and hottest in manual memory management than actually do.
More people think they need a hundred thousand threads than actually do.
If you do have one of those cases, by all means prepare for it and deal with it. But be sure you have one first. The program that exceeds so much as a 100 threads is not only exceptional, but very exceptional. The exceptions are cognitively available and leap to mind, but are nevertheless the exceptions. And, again, if you have one, deal with it, but be sure you have one.
If you're sitting there in TypeScript land writing "async" code you've already surrendered on Ultimate Efficiency anyhow. Deciding what is more efficient between a threaded program in a runtime that doesn't box everything and JIT-optimized JS code is difficult but it isn't that hard for the threaded program that isn't boxing to win out on all runtime measurements, including consumed RAM.
"You write "index = last_index++;" and it Just Works (tm), no need to worry about locks or atomics or other thread access."
That goes back to my comment about using better threading techniques. I write a lot of "index = last_index + 1" (mutably incrementing like that in a single expression is just bad style anywhere you see it) in my threaded code all the time without thinking much about it, because I use the model where by default a value belongs to the one actor process that has access to it at all. The problem isn't that mutation is dangerous in threaded code, the problem was people writing threaded code based on a ton of threads running around shared data structures with locking. Not only is that not the only way to write threaded code, it is literally the worst. There are many other options, all of them better in some way, many of them much better.
Contrasting the difficulty of writing threaded code to something else based on the assumption that "lots of shared state locked by semaphores" is the only way to write code is like a Haskell advocate talking about the amazing benefits of functional programming while writing as if literally every imperative program is just one big pile of unmitigated, pure spaghetti code where everything is linked together with gotos and every variable in the program is a global variable. That's not the relevant comparison any more. It hasn't been for a long time. If anyone's program is scrambled because they did write a big pile of gotos and global variables or they did write a big pile of shared state with semaphores everywhere, that's on them. A vast array of better techniques of all shapes and sizes was available to them.
> More people think they need a hundred thousand threads than actually do.
This is fair, but is a hundred thousand OS threads really a reasonable load on reasonable OSes?
I've (helped to) run systems with millions of Erlang processes on a node, so I'm not most people and my attitudes might well be skewed. I would expect that the reasonable ceiling for OS threads is closer to 10k than 100k, although I don't think I've seen many articles about findinf the limits... maybe everything just quietly works?
If the limit is 10k, I think there's a lot of real situations where thread per connection or thread per task runs out of headroom. If the limit is 100k, a lot fewer people are going to hit that. Common wisdom is "thread per connection doesn't scale", but it has a desirable programming model, so the question becomes how to get the programming model and scale, at least to modest size.
If it's fine to mostly just use threads (hopefully without so much of the shared everything model that makes everything hard), it would be great if people knew that instead of spending so much time adding async/await to everything. :P
The principal difference between dispatching in a thread-based framework and async/await is that async/await allows you to program sequences of asynchronous operations much more easily. No more separation of code that initiates an async operation and the code that handles the result!
These higher level primitives that they mention provide the primitives for exactly that. This can be found in other paradigms besides async/await.
I do find that the ergonomics of this are highly dependent on a few features of a language runtime, without which it all falls apart. Or you need language specific syntax and typically a single standard implementation.
Of course. I don't think a language can support async/await without a library implementation, or the language features that support it.
And I can't honestly think of another paradigm that doesn't require callback functions or lambdas that, ergonomically, end up producing function implementations that end up drifting off the right side of the screen for anything more than a couple of sequential asynchronous operations.
I didn’t say async/await need language features (though they usually do), I meant that high level async orchestration can be represented with suitable language features - often in ways that are more interesting than async/await.
“I can't honestly think of another paradigm that doesn't require callback functions” … Haskell, Scala, Rust all use various approaches to asynchrony that leverage these language features to provide you very usable abstractions without the “callbacks” you mention. Some of those lean on lambdas, but the scrolling off the right issue hasn’t been an issue there for at least a decade now.
Scala’s direct mode is interesting, as an example of library driven, blocking/imperative style interactions that provide most of the benefits of the monadic effect style, but in a way that’s far easier for a human to write and review.
This might not be ready for mass adoption yet, but I think it’s a sneak peek of where we’ll see some languages move to.
Very much so and it can be argued that the difference between threads and ssync is just another dimension to compare on. For example, essentially everything that is involved in Structured Concurrency is as relevant to parallel scenarios as well.
You get a few dimensions. Some of the dimensions listed in the article for async/await become user library decisions rather than being baked into the threading. But you could still get things like, is each thread memory-isolated (Erlang, Pony?) or not, can you cancel them (imperative languages no, but Erlang and Haskell yes), is the concurrency structured or not, details around how and when the threads clean up (though perhaps arguably more related to memory management then thread management), can a thread be pre-emptively descheduled (though I'm not sure if there is any current system where the answer is "no", Go was "no" for a while).
If I sat down and made a careful study of all the threading implementations we might get up to a similar number of quirks.
I would suggest though that the dimensions are generally more likely to be corner cases. Some of the dimensions mentioned in that article are fairly in-your-face for an async/await implementation and can cause serious difficulties migrating between systems fairly quickly if you make the wrong assumptions, and writing correct async/await code that isn't just straightline "await everything immediately" code has to start taking some of those things into account very quickly. The equivalent for threading is more likely to only come up rarely and in more cases the correct answer is really "don't depend on that anyhow", e.g., rather than depending on exact details of how a thread is terminated to accomplish something, just cleanly send a message with your results to whoever it is waiting for it directly and let the runtime do the cleanup without your code witnessing any effects of it. Depending on these quirks in threading code is much more likely to be bad engineering practice, rather than necessary engineering practice in the async/await case.
> What is "await" if not a synonym for "suspend"?!?
The `await` keyword in most languages is not a synonym for suspending thread execution so much as it is an effectual attempt to replicate the functionality of `coreturn`[0]. To wit, if an underlying `Future`/`Promise` has completed before the `await` instruction is evaluated, the thread executing same will not be suspended.
> What is "await" if not a synonym for "suspend"?!?
There are scenarios where something might need to await and might not. Why take the hit if you are able to do something synchronously? Edit: this is especially important given the “viral” nature of colored functions.
It does make it hard to reason about, but this kind of problem is all over the place - e.g. very similar-looking code can have very different semantics depending on your framework if you’re using jsx or a particular decorator means one thing in one project and something else in another. That’s just part of the game at this point.
C# has Task.FromResult(), which is useful if you are implementing an interface that allows async work but your implementation doesn't require it. I believe the runtime will check for this case and continue execution. It's better for the cache to keep executing the current task on the current thread.
I don't really understand gp's point. From inside the code, you can't tell if there was a pause or not. Clock time or thread id are heuristics, but you can't really be sure.
I'm teaching async calls in makearcade to my 10yo son, to bypass a platform bug. He said he doesn't get it. My answer was: Don't worry, adults don't get it either.
Given that the median household income in the US is $83k, I don’t think that losing $1k on a busted phone is likely to put many into “spiraling debt slavery”
Is United Airlines “just about making more money”? Yes. Has it done so by offering people a valuable product and generating massive consumer surplus? Also yes.
It’s very difficult for the average person to use a ten year old browser; in fact I’d offer that the only way to use a ten year old browser is to be an expert and do so intentionally.
There are plenty of people with old android phones with no free disk space using ancient browsers.
There are plenty of people still using windows 10 with updates turned off or wedged for whatever reason.
These people just use the sites that work. They aren't computer experts, and might not even realise why half the internet doesn't work - they just think that's the way things are.
I think you're conflating "old device" with "10 year old browser" here. E.g. for:
> There are plenty of people still using windows 10 with updates turned off or wedged for whatever reason.
It'd be "the pool of people who installed Windows 10 immediately in the launch year but somehow accidentally blocked their browser from updating in the 10 years since, weren't able to fix the issue as the web slowly stopped working, and are stuck using that computer anyways" not "the pool of people still on Windows 10".
The latter won't have many non-intentionally pushed into "10 year old browser status" until 2038 at the earliest.
When will Microsoft stop doing Windows 10 security updates?
I have a 10 year old laptop with 32GB of RAM, GTX 970 6GB and an SSD.
For many things it is better than any 16GB work issued laptop (that often come with integrated cards - so you wont be able to run any AI model on them). Although the old ssd is starting to show its age (perhaps a full system reinstall would solve this, other option is to get a new one).
The old laptop does not have UEFI so it could not get the (free for some time) Windows 10 to 11 upgrade.
I am smart enough to install Firefox on it and update it, but the official Windows 10 updates will stop coming soon.
I was effectively kicked out by Microsoft because my device is "old". Even if it is beefy enough to browse the internet and watch youtube.
Note that I bought a new beefy laptop now that I hope to use for the next 5+ years (hopefully more), but who knows if they wont come out with some new idea, like UEFI 2.0 for Windows 12 - that again will mean we need to buy new hardware and new windows.
On an unrelated note I want to turn the old laptop to a linux machine - for fun and learning, but dont have the time for that.
> When will Microsoft stop doing Windows 10 security updates?
Last October, unless you are on an LTS type version - in which case somewhere between 2028-2032 (depending on the exact version). Edge will still update until at least 2028 even though the OS stopped receiving updates... though I'm not sure I would wish either of those usage scenarios on someone :).
> The old laptop does not have UEFI so it could not get the (free for some time) Windows 10 to 11 upgrade.
The free registration, they never actually axed the program at the end date https://techcommunity.microsoft.com/discussions/windows10spa.... Just make sure you use the same edition (e.g. Pro -> Pro). You also don't have to do an in place upgrade to do it. In your specific scenario, you would have to bypass the install requirements in the installer to get around the lack of UEFI though.
Hope that helps, Windows 11 is definitely a bit of an annoying step (even once it's installed).
While I agree with your general gist and definitely your final paragraph,
> There are plenty of people with old android phones with no free disk space using ancient browsers.
How many people have 10 year old phones? I've got an 8 year old iPhone XR which I keep around as a backup/travel device because it's not worth selling, and the battery is… not happy even in airplane mode.
For me to have a 10 year old mobile browser, I'd have to have kept the iPhone SE 1 (or was it a 5c?) that I bought second hand in 2018, and not upgraded it since I bought it. I got rid of it because the battery wouldn't hold a charge for 10 minutes.
I have a 10 year old cell phone that still gets regular use. Works just fine for things like phone calls, texts, youtube (newpipe), termux, and note taking. Original battery isn't great at this point, but a new one is maybe $15. Zero reason to replace it.
I've a Xiaomi Mi 6 phone (2017 model) that I still use as a fridge-mounted shopping list and it's using the latest version of Chrome. I think it would be quite the stretch to find a user using a 10 year old browser.
It's fine to support such configurations by accident, but you shouldn't try to support them intentionally. You will end up dropping support eventually regardless but the skeletons will live on in your codebase as tech debt.
The needs of the many outweigh the needs of the few.
Eventually you are making things worse for your vast majority of users when you have to e.g. make them install a native app for a video call or use a TLS version that is broken to support those Gingerbread Android phones
or windows 8[.1], or windows 7, or windows xp... there's a lot of old hardware out there, not every is rich/tech savvy (see also: old people) enough to purchase a new device even 10 years later
I'm not sure this is a realistic use case to try and support. A 10 year old android phone likely has a battery life measured in 10s of minutes, and really isn't something we need to worry about.
I'm currently using >7 year old Android phone. The batteries on these things are child's play to replace (especially after you've done it the first time and removed the pointless adhesive in the battery compartment). I will consider upgrading once new devices return to feature parity with my current device (apparently never).
While I'm of course an edge case, the fact that Google, Apple and Samsung all provide >5 years OS support for devices now (and battery replacement services) suggests that many people hang onto their old phones for a long time.
That said, I'm not using 7 year old software on my phone. That would be insane. And my browser (fennec) was updated just a few days ago.
You get the guy at the mall to swap in a new battery for $50 in most parts of the world. Its cheaper to do that every few years than buy a new phone, and I have several family members who refuse to upgrade on principle, because modern phones grew too large for their hands/pockets
There’s also being poor, or working for an organization that’s poor. In both cases the obsolete(?) software might be various degrees of intentional, but the alternative is usually worse anyway.
I support a bevy of older people with older computers, senior-citizen types. Upgrades are expensive. Monetarily, but also in retraining. These folks don't want the latest UI, they want what is familiar, and retraining is super annoying.
Computers that were EOL a few years ago, running ten-year-old browsers, are absolutely routine.
That's a choice by the people who make websites and browsers that forces the average person to buy a new computer. If we all cared about letting people use old computers, this wouldn't be the case.
I doubt that for the hackernews audience that the age of the browsers is an issue. I would say in practice that 90% is nowhere near what is achieved - that it's closer to 90% and amongst the hackernews audience probably lucky if it gets to 50% because of our use of anti-tracking and ad blockers.
Respectfully, you may live in a bubble of fairly tech-savvy folks. Most of my extended family run 10+ year old laptops as their daily drivers. Their phones are often on the second or third battery replacement. They don't install updates very often (if at all). For the most part they are still more proficient with tech than many of their peers.
This could also be a story of technological progress. A thought experiment - imagine you, an archaeologist, recovered the remains of our civilization, from roughly 1925 to 2025, but the only surviving artifact was televisions. You know that televisions are valuable - initially only wealthy families had them - so you used them as a proxy for riches and plotted the Gini coefficient using just the size, quality, resolution, color depth, etc. You could conclude that our society became less unequal over that period, because you miss that technology dramatically compressed the distribution of this resource and that household wealth was freed up to put to other purposes.
If we had supporting documentation supporting "initially only wealthy families had them" why would we not also have supporting documentation supporting "eventually average families had them?"
Seems like the entire "initially" premise kind of indicates the change, no?
What's interesting to me as someone who has sold a lot of software to a lot of software companies is that many enterprise vendor agreements explicitly allow companies to pentest their vendors with advance notice and coordination. I don't think any of our clients ever exercised that clause; I expect it's going to be exercised a lot more going forward because it's so easy to do now.
reply