40 ms·
Not specific to rust, but I think asynchronous programming in general is a hype. It didn't start because it is so awesome, it started because JS can't do paral
by usrbinbash 3y ago
Not specific to rust, but I think asynchronous programming in general is a hype.
It didn't start because it is so awesome, it started because JS can't do parallel any other way. That's the long and short of it. People wanted to use JS in the backend for some reason. The backend requires concurrency. JS cannot do concurrency. Enter the event loop. Then enter some syntactic sugar for the event loop. And since JS is popular, async became popular.
Code written using threads is, at least to me, much more readable and easier to reason about. Each section in itself is just synchronous code. The runtime/kernel take care of concurrency. The overhead is negligible in a day when we have greenlet implementations. It works for both i/o bound concurrency and cpu bound parallel computing. It doesn't require entire libraries rewritten to support it. There is no callback hell. It scales both horizontically and vertically. Modern languages support it out of the box (Hello `go` keyword).
I realise that this is going to get a lot of downvotes. I don't really care. To me, async is just "cooperative multitasking" with a quick paintjob. We left behind that paradigm in Operating Systems decades ago, and with good reason.
- 3np 3y agoYour comment seems to be conflating concurrency with parallelism. JS doesn't have any language-level abstractions for parallelism (async or not) but you do have Web Workers[0] and process forking (depending on runtime) to get actual parallel programming. JS async deals with concurrency, not parallelism. Threads are the opposite: They are interfaces for parallel programming and their use is orthogonal to how your application handles the resulting concurrency. You say "the runtime/kernel take care of concurrency" - are you telling me you never write a mutex or implement locking logic? Because that's what "taking care of concurrency" is. I'd choose refactoring callback-/Promise-hell over debugging a complex deadlock any day (unless intra-process parallelism is actually a requirement, which may tip the scale in the other direction). In the context of doing concurrency and parallelism in Rust, I'd 100% agree that the JS/C#-style async/await approach isn't necessarily always the best approach and it's good to consider alternative idioms if your requirements call for it. For anyone writing "apps" or glue-libraries, though, I'm thankful that they stay away from spawning threads all over my system by default and that they need more tools than "learn Rust async in 15 minutes" gives them to become dangerous. Messing up your single-threaded event-loop concurrency can hog roughly a single CPU core and cause OoM. Messing up thread-based concurrency can have larger implications on the hosting system. [0]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers...
- deleted 3y ago[deleted]
- ngrilly 3y agoasync/await doesn't entirely remove the need for mutexes and locks. We still need them if we have multiple coroutines using a shared resource across multiple yield points.
- pkolaczk 3y ago> We still need them if we have multiple coroutines using a shared resource across multiple yield points. We still need them if we have multiple parallel tasks (coroutines spawned non-locally) using a shared resource across multiple yield points. As long as the accesses to the shared variable are separated in time, sharing is fine. This is correct code: let mut foo = 1; async { foo += 1 }.await; foo += 1; println!("{foo}"); See - a shared variable used across multiple yield points. Another (more useful) example I showed below in another post with `select!`.
- gpderetta 3y agothe equivalent threaded code wouldn't need a mutex either: int foo = 1; std::thread ([&] { foo+=1; }).join(); foo+=1; std::cout <<foo <<'\n'; (sorry for the C++, I don't speak much rust).
- pkolaczk 3y agoPoint taken. What about this pattern (pseudo code, obviously it would require e.g. adding some code for tracking how much data there is in the buffer or breaking the loop on EOF, but it illustrates the point): mut buffer: &[u8] = ...; loop { select! { _ = stream.readable() => stream.read(&mut buffer), _ = stream.writable() => stream.write(&mut buffer), } }
- gpderetta 3y ago
- pantulis 3y ago> it started because JS can't do parallel any other way I remember doing async in C++ with CORBA and ACE's Reactor pattern about 25 years ago, it was not beautiful nor easy. But if memory serves, most interpreted server side languages used for web programming in the late 2000's didn't have mature multithreading or async capabilities and most of the deployments consisted on exec/forked full application servers. I would also bet that this is exactly what made Node.js popular. To each its own, async is just another tool in the proverbial belt.
- Animats 3y agoThere's a role for async, and it's when you're very I/O bound, you have one thread, and async means you don't need locking. This is simple to think about. That's the classic JavaScript model. If you have compute-bound components, things get more complicated. If you have threads, locks, and async, all in one program, things get much more complicated. I'm not sure that's a win. At some point, it's easier to use something like Go's green-thread "goroutines", which try not to block, but can block if they have to.
- starcraft2wol 3y ago> when you're very I/O bound, you have one thread Yes, but this is very unusual. In a web server, you have pool of threads that can respond to incoming connections. When one is blocking, another is ready to go. All the transition are handled transparently by the kernel.
- Animats 3y agoIn node.js, each process used to be single thread. There's now a hokey threading model with limited shared memory areas.[1] Annoyingly, this is also Android's threading model. [1] https://nodejs.org/api/worker_threads.html https://nodejs.org/api/worker_threads.html
- sirwhinesalot 3y agoStackless coroutines are pretty useful to model state machines. I particularly like generators (yield) to model lazy evaluation and iterators. I do agree however that async as a concurrency approach sucks.
- rapsey 3y agoRust does not have a runtime and making the kernel take care of it is not remotely as efficient.
- bombolo 3y agoThreads are way slower than event loop. Having 2-3 threads with their event loops is massively faster than having every task running on their own thread.
- fvncc 3y agoOne advantage of async/await is that its easier to cancel things. For example, this leads to the design pattern where you have multiple futures and you want to select the one that finishes first and cancel the rest. In regular threaded programming, cancellation is a bit more painful as you need to have some type of cancellation token used each time the thread waits for something. This a) is more verbose and b) can lead to bugs where you forget to implement the cancellation logic.
- charcircuit 3y ago>In regular threaded programming, cancellation is a bit more painful No, it isn't. Nothing is stopping your threading library from implementing the same thing. It just turns out it is a bad idea to kill threads at random points in time because they may own things like locks. Or in the case of async await doing something that is thought to be atomic.
- Spivak 3y agoYou're describing exactly why it's painful for threads. If you only cancel co-routines at yield points (which unless you do dark magic is the only time you can cancel them) is always safe. A co-routine that yields in the middle of an atomic operation is an oxymoron. Anything can happen before you're scheduled again.
- fvncc 3y agoWell each time you use the await keyword you are saying its a safe point to exit, which is more predictable than killing at random points. Holding locks across await points is an anti-pattern, and Rust at least can give a hint if you try to do that. Async/await implementations will also generally allow you to run cleanup code on cancellation (but the exact mechanism depends on the language). In the end, its about expressing a state machine in a more concise implicit way, which is a suitable level of abstraction for most use cases.
- heavenlyblue 3y ago> One advantage of async/await is that its easier to cancel things. For example, this leads to the design pattern where you have multiple futures and you want to select the one that finishes first and cancel the rest. > In regular threaded programming, cancellation is a bit more painful as you need to have some type of cancellation token used each time the thread waits for something. This a) is more verbose and b) can lead to bugs where you forget to implement the cancellation logic. Yeah of course Rust just makes cancellation so easy by allowing the Futures to be dropped. What about the resources these Futures could have allocated within the context that are not just memory? You are saying it as if async/await somehow solved the whole problem of stack unwinding.
- wruza 3y agoTo me, async is just "cooperative multitasking" with a quick paintjob It is, and not only to you. It is a way to save a call stack until a runloop calls it back. But what I can’t agree with is parallels with OS. Coop MT is only problematic in OS MT. When it’s your code there’s no unknown bad actor, and having multiple cooperative (mostly waiting) processes without scheduling them on a thread pool is a useful concept regardless of threads availability. E.g. when you have to wait on multiple sources, the options you have are: - serialize - perform few non-blocking calls and wait for any/all of them to complete - schedule them as tasks on a thread pool and wait for their completion Async can do all three, it’s orthogonal. I’d say that just awaiting on PMT task completion is much more convenient that setting up locking primitives. Same for NB polling. Promise is just an abstraction and all it does is waiting for an event to fire on a current thread’s runloop while retaining the comfort of a lexical scope, all with a couple of keywords.
- hayley-patton 3y agoBut I'm my own worst enemy, and blocking the event loop is unpleasant even when I do it myself. Once I had to pass compute-intensive tasks (hashing some data, which took long enough to matter) to a thread pool, to not hurt latency for other tasks in the event loop. > I’d say that just awaiting on PMT task completion is much more convenient that setting up locking primitives. Don't use the primitives then - write e.g. a parbegin/parallel-map atop thread primitives, and use that.
- amelius 3y agoAsync is great for dealing with I/O but it forgets that the CPU is a resource too.
- Spivak 3y agoWhich is why you queue up CPU intensive tasks to threads where you can time slice, and IO intensive tasks to async/fibers whatever you wanna call them. It's hard to make this completely seamless. Power to Java for getting the closest.
- tbillington 3y agoFundamentally, async/await and threads are different tools. Async/await is "in vogue" at the moment, but there are still real advantages in certain scenarios. For example, the blog authors project is an OS running on minimal resources that would not be appropriate for any threading model I'm aware of. This is a wee operating system written to support the async style of programming in Rust on microcontrollers. It fits in about 2 kiB of Flash and uses about 20 bytes of RAM (before your tasks are added). In that space, you get a full async runtime with multiple tasks, support for complex concurrency via join and select, and a lot of convenient but simple APIs. Rust actually used to have green threads before 1.0. You can read about the proposal and reasoning for it's removal here https://github.com/rust-lang/rfcs/blob/master/text/0230-remove-runtime.md https://github.com/rust-lang/rfcs/blob/master/text/0230-remo.... If you'd like more info on the story around the adoption of async/await in rust you can see this excellent talk by Steve K. https://www.infoq.com/presentations/rust-2019/ https://www.infoq.com/presentations/rust-2019/.
- justin_ 3y agoAsynchronous programming is a great fit for IO-driven programs, because modern IO is inherently asynchronous. This is clearly true for networking, but even for disk IO, generally commands are sent to the disks and results come back later. Another thing that’s asynchronous is user input, and that’s why JS has it. As for threading vs. explicit yielding (e.g. coroutines), I’d say it’s a matter of taste. I generally prefer to see where code is going to yield. Something like gevent can make control flow confusing, since it’s unclear what will yield, and you need to implement explicit yielding for CPU-bound tasks anyway. Its green threads are based on greenlet, which are cooperative coroutines. Cooperative multitasking was a big problem in operating systems, where you can’t tell whether other processes are looking for CPU time or not. But within your own code, you can control it however you want!
- mgaunard 3y agoThat's not how an operating system models disk access though. You synchronously write to the kernel cache, and the kernel eventually gets those written to disk. Wanting to do asynchronous I/O to disk is only useful if you're aiming to bypass the cache. In practice it is very hard to reach higher performance when doing that though.
- justin_ 3y agoI was referring to the fact that interaction with the disk itself is asynchronous. Indeed, the interface provided by a kernel for files is synchronous, and for most cases, that's what programmers probably want. But I also think the interest in things like io_uring in Linux reflect that people are open to asynchronous file IO, since the kernel is doing asynchronous work internally. To be honest, I don't know much about io_uring though - I haven't used it for anything serious. There's no perfect choice (as always) -- After all, for extremely high-performance scenarios, people avoid the async nature of IO entirely, and dedicate a thread to busy-looping and polling for readiness. That's what DPDK does for networking. And I think for io_uring and other Linux disk interfaces have options to use polling internally.
- mgaunard 3y ago
- pkolaczk 3y agoasync/await allows to do concurrency without the need for explicit synchronization to shared data structures. E.g. I can do: loop { select! { _ = src_channel.readable() => src_channel.read(&mut buffer), _ = dst_channel.writable() => dst_channel.write(&mut buffer), } } without any mutex guarding the buffer, even though the reads and writes happen concurrently and share the same mutable buffer. This is possible because with async/await the concurrency is cooperative, the code precisely controls where context switches can happen (in this case this is the select! waiting for event), and the compiler can see that even though the code as a whole is concurrent, the branches of select do not run at the same time in parallel. This is not possible to achieve with threads directly. If using blocking I/O + threads model, then you'd need to dedicate one thread for reading and one for writing and then synchronize access to the shared data structure (where using a queue/channel also counts as synchronization). Which obviously would be much harder to get right.
- gpderetta 3y agoSCHED_FIFO
- usrbinbash 3y ago> Which obviously would be much harder to get right. Not obvious to me I'm afraid. Using CSP, this is almost trivially easy. All access to the data goes through a guardian thread. Accessing the resource is just sending a message. And mutexes are not hard either.
- cmrdporcupine 3y ago+1. This approach of having guardian threads/actors etc communicating by messages is just a natural for Rust, too, because it nicely deals with a whole pile of borrow checker & lifecycle issues, too. crossbeam_channel FTW
- cmrdporcupine 3y agoThis is great until someone calls you through with a multi-threaded task runner and you've just gone and added consistency and race conditions to your code.
- samsquire 3y agoCoroutines, async, parallelism and concurrency is my main hobby. Business logic programmers shouldn't be dealing with machine level parallelism and async unless heavily abstracted, such as in a job queue or evented message queue. JMP or RET is how the machine transfers control flow at the machine level. So coroutines are a natural solution to switching between code at the machine layer. If you're working in Javascript, then you shouldn't have to worry about this stuff. Cooperative multitasking is elegant, within a process for scheduling but not as the main approach for the operating system to switch between processes, it's a subtle difference. If the operating system depends on cooperative multitasking, some buggy processes can keep control flow to themselves. But using cooperative multitasking inside a process for code elegance, is a good way of scheduling and decoupling concerns. I have a lightweight thread runtime similar to Go and I find event loops really interesting, I want to make the pain of async and parallelism go away.
- jayd16 3y agoDo you think async/await started with JavaScript? This take is pretty revisionist.
- motbus3 3y agoOthers have already answered to you, but maybe a bit more affirmation may help. Concurrency and parallelism are different concepts. Many many years ago it was easy to confuse them both because there were no parallelism. You had a single big core and CPU pipelines were much simpler. I won't delve into details, although I found them fascinating, but concurrency and parallelism are different tools. I confess I found the name "concurrency" not useful. Concurrency allows you to transfer control from one piece of text (I mean executable code) to another while it waits for the return. Parallel means instructions are being executed, well, in parallel. The OS scheduler does not inspect the code, nor know that the next instruction will be a noop sleep. Some languages with runtime environments provide basically functionality pra intercepting calls and nudging the OS. Attempts in the past of requiring each application to be clear about sharing control but it failed. One single bad application could hang and compromise the entire system. As a matter of fact, some RTOS uses this premise of development. Async has been implemented by providing a runtime library which saves the context and swap tasks. The control is only hidden from the programmer. I do not know about Golang, but I suppose coroutines are implemented in a different way, as it seems to me, that the compiler handles this. But I don't know.
- mjan22640 3y agoThis is an occurence of co-Blub paradox https://reasonablypolymorphic.com/blog/coblub/index.html https://reasonablypolymorphic.com/blog/coblub/index.html
- psychphysic 3y ago> And it’s not hard to see why; while humans have dedicated neural circuitry for natural language, it would be absurd to suggest there is dedicated neural circuitry for fiddling around with the semantics of pushing around arcane symbol abstractly encoded as electrical potentials over a conductive metal. Huh? It's certainly the case that people who program have dedicated neural circuitry. Most of the work is not being done by language centres[0] [0] https://hub.jhu.edu/2020/12/17/brain-activity-while-reading-code/ https://hub.jhu.edu/2020/12/17/brain-activity-while-reading-...
- deleted 3y ago[deleted]
- qwertox 3y agoFor me async/await was a godsend. I've had so bad experiences with multithreading in C++, where I ended up in a Semaphore/Mutex hell in a big project, that async/await is a really welcome compromise between using idle CPU resources and just not doing multiple things at once. Then there's `#[tokio::main(flavor = "multi_thread", worker_threads = 2)]` which actually lets you use multithreading with async/await which is just a nice feature to have, given that Python can't do this due to the GIL. But asyncio in Python is also so great.
- AndrewDucker 3y agoIt began in C# in 2012 - 5 years before JavaScript. And C# does threads. And made its way to JavaScript via Typescript (whose creator also created C#).
- S04dKHzrKT 3y agoMaybe I'm misunderstanding what you mean by "It began in C#", but F# introduced async about five years before C# 5 was released.
- q8840 3y agocould be wrong about c#, but c# and f# are comes from same parent
- AndrewDucker 3y agoI know it influenced the async await design, but I didn't think it was quite the same. Will go do some reading!
- louthy 3y agoasync in F# is not a language feature, it’s a library that leverages F# computation expressions (monads). It’s also possible to do async-like behaviour - without the async/await language feature - in C# using LINQ; so you could argue C# has had the capability (like F#) since LINQ was released. But, I believe C# was the first mainstream language to implement the async/await method-splitting coroutines state-machine (as a language feature)
- gpderetta 3y agoAsync is a special case of continuations. If you have fist class continuations (and monads do notation in practice gives you that), you hardly need async as a language feature.
- louthy 3y agoThat's exactly my point. It's not a language feature, it's a library. Haskell, F#, and any other language that supports monads (or as you say, first class continuations), have the ability to do async/await - in a way that appears first-class - but actually is just regular code. C#, and other languages that have taken the C# approach [to async/await], don't have first class continuations (well, C# does with LINQ, but that compromises most ways the average OO dev works). They implement async/await with first-class keywords that indicate where to slice a method in two. In my language-ext [1] project I have added the LINQ operators to `Task<T>` which allows C# tasks to be used in the same way that Async is done in F#. [1] https://github.com/louthy/language-ext/blob/main/LanguageExt.Tests/TaskTests.cs#L73 https://github.com/louthy/language-ext/blob/main/LanguageExt...
- phicoh 3y agoIn C, I have written and worked on a lot of code that is event-based. Async is a natural extension to that. So, no it is not a hype. These types of techniques have been used for a very long time.
- usrbinbash 3y agoAllow me to clarify: Event Loops are an old technique and not a hype. I am aware of that. One of my earliest C experiments as a kid was writing a snake-implementation for the terminal. I still have that code, and it uses an event loop to process the input. The hype I am talking about, is an assertion currently "en vogue", that asynchronous should be the default method of doing concurrency. This hype, imho, started with the ubiquitous use of JS as a backend implementation language. JS couldn't work another way, so this is what all these JS programmers used, and JS is super popular. So, it must be cool and great. And that's how a workaround for a language limitation became a popular paradigm.
- 3np 3y agoYour rant reminded me of this classic post, which I believe shares your views but from different reasoning. From the discussion, you may be interested in looking at zig[0]. https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ https://journal.stuffwithstuff.com/2015/02/01/what-color-is-... https://news.ycombinator.com/item?id=36597229 https://news.ycombinator.com/item?id=36597229 (fresh repost) Discussed previously: https://news.ycombinator.com/item?id=8984648 (8ya) https://news.ycombinator.com/item?id=16732948 (5ya) https://news.ycombinator.com/item?id=23218782 (3ya) https://news.ycombinator.com/item?id=28657358 (2ya) [0]: https://kristoff.it/blog/zig-colorblind-async-await/ https://kristoff.it/blog/zig-colorblind-async-await/
- cgh 3y ago[Somewhat offtopic] Speaking of Zig, I understand concurrent programming is undergoing a rethink of some sort as async has been temporarily removed? I am only following cutting-edge Zig peripherally so I am probably wrong here. Does anyone know what Zig’s future concurrency story is?
- mzimbres 3y agoImplementing full-duplex protocols asynchronously is much simpler.
- hot_gril 3y agoThe alternative with threads on an IO-bound server eventually cycles back to async/await but with extra steps. You write synchronous request handlers until you notice IO-waits wasting all your thread time, add more threads to the pool, start hitting overhead from that, then implement greenthreads. NodeJS did it right, and it's hard to call a 14-year-old technology a fad.
- starcraft2wol 3y ago> add more threads to the pool, start hitting overhead from that I would like to know more detail about this claim. The scenario you are describing is one were 64-128 OS threads are fully blocked waiting for IO. If that's the case, is it likely that you will have additional unused IO resources that could be being utilized? Also, what overhead do you see as the main limit on spawning a lot of threads? Is that the CPU time of context switching? If so, in this scenario CPU is not the bottleneck, and switching between processes will be nothing, especially with a 32-64 core CPU. This is a genuine question as I have never worked on an application that got close to maxing out either approach.
- hot_gril 3y ago> The scenario you are describing is one were 64-128 OS threads are fully blocked waiting for IO. If that's the case, is it likely that you will have additional unused IO resources that could be being utilized? One likely scenario is that you've issued 128 RPCs to some other services and are waiting to hear back. Even if each RPC is, say, on a separate TCP connection, your network stack can handle plenty more. > Also, what overhead do you see as the main limit on spawning a lot of threads? Is that the CPU time of context switching? If so, in this scenario CPU is not the bottleneck, and switching between processes will be nothing, especially with a 32-64 core CPU. I don't remember what specific feature of OS threads contributes the most to overhead, and maybe someone else can answer this better. But context-switching burdens both the CPU and RAM (due to saved stacks). > This is a genuine question I always assume this anyway. Maybe not on Reddit ;)
- starcraft2wol 3y ago
- vmfunction 3y ago>Not specific to rust, but I think asynchronous programming in general is a hype. Hmm, wasn't the whole point of doing things in event loop because it way out perform thread-based architecture? Like when Nginx way out performs Apache? On a single core CPU of course. Edit: It is not that event-loop is better than thread-based, just in a web server scenario it just perform much better.
- arijun 3y agoThe parent comment mentions green threads. While there is some performance hit to using them, I don’t think it is way less performant. I mean, go was built for being a web backend, and is based on green threads. For rust specifically, though, green threads/coroutines were discarded because they are not zero-cost.
- sophacles 3y agoUntil go 1.14 it basically the same as any other async/await under the hood. Every function call included an implicit .await - that is it offered a `yield` to the runtime scheduler. All the io was built around non-blocking/polling, etc. Tight loops in go would potentially screw up your app performance because there were no yields. In 1.14 they introduced some sort of preemption for tight loops too.
- gpderetta 3y agoStackful vs stackless, that's the big difference and the point of the original comment. Stackful abstractions are strictly more powerful of stackless ones (go coroutines subsume async/await but not viceversa). You can have good ergonomics and performance with stackful cooperatively scheduled tasks instead of a stackless sync/await abstraction. Async/await makes sense when you have so many tasks that you cant afford to dedicate a full stack to each of them and segmented stacks or heap allocated frames are not an option (for performance or compatibility).
- diarrhea 3y ago> We left behind that paradigm in Operating Systems decades ago, and with good reason. I'm curious, what reason? I grew up on Python and C#, and only know async/await, never done real threading (C# async is threading and coroutines under the hood, Python is just coroutines, single-threaded). I find that way of writing code very elegant, as one can encode points of blocking/switching explicitly. A bit like encoding logic into the type system (cliffle has an article on the type state pattern, a good read!): the underlying async implementation can change without code adjustments.
- GolDDranks 3y agoThe reason we left that paradigm in _Operating Systems_ is that OS's are supposed to be resilient. A single buggy app could easily freeze/crash the whole Windows 3.1 system, because the system has the naive assumption that all the apps are benevolent, bug-free, and happily co-operate with time-sharing. Try the same in Windows 2000; you can't, because the system is pre-emptive and forcibly ends the time slots of apps that don't yield. (And possibly kills the app; "MyApp isn't responding" etc.) However, that same reason doesn't apply within a single app, because a single app by a single author _can_ safely co-operate with itself. So co-operative time sharing can work and make sense within single app.
- dale_glass 3y ago> However, that same reason doesn't apply within a single app, because a single app by a single author _can_ safely co-operate with itself. So co-operative time sharing can work and make sense within single app. That's not the case in any non-trivial app. Any real program truly has dozens if not hundreds of authors whose code you're using sight unseen, and which may be difficult to modify. And your program getting stuck can be just as bad as the OS getting stuck. Suddenly your program doesn't reply to API requests, or doesn't relinquish some expensive resource (like an expensive VM), or some such.
- usrbinbash 3y ago> I'm curious, what reason? In days gone by, processes who got to run on the single core that contemporary CPU had available, had to actively relinquish control of the core back to the kernel. If a single process refused to do so, e.g. because the program hang, there was nothing the kernel could do about it, and the entire OS was blocked. The scheduler never ran, no other process would get CPU time, the whole thing was dead in the water, and all you could do was kick the "Reset" button (if present) or pull the power cord and reboot. Obviously, this is a very bad situation for an OS, which runs many processes from many sources. And because of that, we ditched this system, and went on to preemptive multitasking, where control is relinquished back to the scheduler after a time whether the process is okay with that or not. Async basically re-invented that system in userspace. We have an event loop, and we have processes that actively yield control to it. What happens if a subroutine refuses to do so? There is nothing the event loop can do about that. And it's really easy for this to happen. All it needs is a single synchronous call, say, to an external datastore, somewhere deep down in the callstack, and the awesome throughput of async goes bye bye.
- dumdumchan 3y agoLook at this program: val another_path = await readFile(path); val data = await readFile(another_path); console.log(data.length); How would you do that using threads?
- anonymoushn 3y agoThis program has no concurrency so you could do it without threads.
- csomar 3y agoNot in Rust if you use something like Tokio.
- anonymoushn 3y agoI'm pretty sure this program has no concurrency even if you port it to Rust and use Tokio.
- csomar 3y agoThis particular one, yes. However, most programs are a bit more complicated than that. That and Async is mostly used for io operations. In which case, the value here is not from non-blocking operations but from freeing up the CPU while this operation finishes execution. You can think of that as another form of executing this program on a separate thread (since the CPU will be freed to do other tasks).
- anonymoushn 3y agoDoing a blocking read also frees your CPU to do other tasks!
- yuvadam 3y agouh, open a thread that does that and `join` on it waiting for it to complete, but why would you need that in your example?
- anonymoushn 3y agoMost languages don't have preemptively scheduled green threads like golang. In Lua and Zig we have "cooperative multitasking" but we get to use the same library for both kinds of applications :)
- secretsatan 3y agoReally weird to throw in JS as the culprit, this is a long standing issue, the asynchronous nature of web work probably highlights it but dealing with asynchronous tasks is part and parcel of writing complex performant applications. Much may be hidden by modern dev environments, but you won't get far beyond the most simple apps before you need to start thinking about how to deal with it.
- flohofwoe 3y agoThe point is that other solutions were able to use traditional threads or green threads (via stack switching) to solve the same problem without the compiler having to transform sequential code into a state machine under the hood (since pre-emptive threads can be suspended and continued at any time, and green threads at specific 'yield-points'). Javascript is limited by the single-threaded browser runtime, and the way that WebWorkers were bolted on later didn't help much because WebWorkers with message passing are too inflexible to implement even green-thread-style task switching. The state-machine code-transformation was indeed the only way out of this dilemma. Async/await is essentially high-level language syntax sugar and it shouldn't matter whether it is implemented via code-transformation, or with green-threads or real threads, or a combination of all those under the hood (but in reality it matters because there's a difference between 'code slices' being scheduled on the same thread or different threads when dealing with shared resources).
- Pesthuf 3y agoEspecially weird because the feature actually originates from C# and was only copied by JS later. And in C#, you can absolutely combine it with Task.Run if you want it to run be concurrent. This feature made asynchronous operations so much better than all the patterns that came before it - and I think they goes for every language that ended up copying it.
- mgaunard 3y agoMy opinion is the opposite, to the point I would argue that anyone advocating for multithreading for reasons other than executing things in parallel on different cores is extremely dangerous and shouldn't be allowed anywhere near a serious codebase.
- psychphysic 3y agoErlang/OTP has entered the chat.
- pltzxy 3y agoAre you talking about Rust (which I don't know)? In Python the async people don't care much about correctness.
- mgaunard 3y agoIt's language-agnostic. The language with the most advanced concurrency memory model is C++.
- GolDDranks 3y agoRust pretty much alleviates these dangers. At least no memory safety bugs because of multithreading. Logic bugs are still possible, of course, but the channel API and the scoped thread API in the standard library do help with those.
- mgaunard 3y agoThat is not true at all, "fearless concurrency" gives no valuable guarantees at all and is widely seen as one of the worst concurrency models in literature.
- GolDDranks 3y agoI don't dispute that, but I thought you were having C++ with threads as the starting point, in which case Rust very much gives you valuable guarantees.
- miki123211 3y agoAsync is, in many situations, better than traditional threads. Threads are a resource hog. They take a lot of system resources, and so you usually want to have as few of them as possible. This is a problem for applications that could, in theory, support thousands of concurrent connections, if not more. With a basic thread-based model, you need 1 thread per connection, and if you have long-lived connections with infrequent traffic, those threads mostly do nothing but consume precious system resources. When you're waiting for data, the thread is blocked and does nothing. With async/await, you can have far fewer threads, maybe even just one, and handle blocking through a system call that wakes a thread up whenever any one of the currently blocked tasks is ready to progress. In languages with much lighter thread alternatives, such as Go's goroutines or Erlang's Beam processes, this problem basically doesn't exist, and so those languages don't need async/await at all.
- flohofwoe 3y ago> Threads are a resource hog. Not really on any decent operating system, but if they are too heavy, there's still fibers aka green-threads aka stack-switching (which at least on Windows are an operating system primitive - but can be implemented in user code on any system that gives you direct access to the CPU stack and registers). I doubt that the async-await state machine code transformation which 'slices' sequential function bodies into many small parts which are then jumped in and out frequently is any better for performance than stack-switching (in async/await you still need to switch a 'context pointer' on slice-entry/exit instead of the stack pointer). One obvious advantage of the state-machine code transformation is that it also works in very limited single-threaded runtime environments without access to the callstack (like WASM). In any case, from the user perspective, async/await should just be language syntax sugar, how it is implemented under the hood ideally shouldn't matter (e.g. it should also be possible to implement it on top of a task scheduler that runs on fibers or threads instead of a state-machine code transformation).
- gpderetta 3y agoThe async/await model gives you exactly one guarantee: because the yield continuation is second class, at most one stack frame can be suspended, so the the amount of space that needs to be reserved for a task is bounded and potentially can be computed statically. This can be important for very high performance/very high concurrency programs, so I think the upsides can be more than the downsides in something like rust, C++ [1], and possibly C#. I still do not understand why async was deemed appropriate, for example, in python. As an aside, there is a lot of confusion in this thread between general async operations and async/await. [1] but of course C++ screwed it up by requiring hard to remove allocations.
- zetalyrae 3y ago`epoll` was added to Linux in 2002.
- cassepipe 3y agoComing from C and having used thread and being totally ignorant to javascript, this article really helped me grasp async in js by clearing away implicit misconceptions I held : https://medium.com/young-coder/5-misconceptions-about-asynchronous-code-in-javascript-ebad2738766 https://medium.com/young-coder/5-misconceptions-about-asynch... And also that talk on the event loop : https://www.youtube.com/watch?v=cCOL7MC4Pl0 https://www.youtube.com/watch?v=cCOL7MC4Pl0
- littlestymaar 3y ago> Code written using threads is, at least to me, much more readable and easier to reason about. It's “easier” because it lies to you and makes you assume that everything is sequential, but it's not, and sometime that “everything is sequential” abstraction is leaky, and you can't really see what's going on without diving to the bottom of every functions. I've been accustomed so much to the transparency of async/await, that I now whish we had the same kind of thing for functions using blocking syscalls (for instance you could annotate the function with the `blocking` keyword and need to use `block` to call it) so you known you need to spawn a new thread if you don't want to wait until the completion of some I/O-bound function.
- starcraft2wol 3y ago> “everything is sequential” abstraction is leaky, Can you explain what details leak? The sequential model was developed for programming because that's a natural way to reason about proccesses. `if then else`. `do this, then do that. The "async/await" designers seem to agree, as they attempt to tame async by emulating this behavior. Note that to do anything other than sequential is extremely complicated to reason about, not because of computers, but because of logic/math. All sorts of concerns like: race conditions, synchronization, dead lock, etc are inherent. Any approach that does not directly address these issues is the one that's creating a leaky abstraction. > so you known you need to spawn a new thread if you don't want to wait until the completion. All functions take "blocking time" to execute. It's a spectrum of how long you want to wait.
- littlestymaar 3y ago> Can you explain what details leak? You answer half of it a few lines later: > All sorts of concerns like: race conditions, synchronization, dead lock, etc are inherent. > Any approach that does not directly address these issues is the one that's creating a leaky abstraction. By writing `await` you're telling your reviewers, coworkers and even your future self than your program stops executing sequentially at this step, and that other concurrent task can do things in the meantime. When using blocking code, the same thing can happen, but this is hidden from you. But in my perspective as a back-end engineer, the biggest issue is related to latency: with annotations you know (and tell others: code is written once but read many time) what takes significant time, with threads and hidden yield point you don't. It looks sequential, but the latency is an observable behiavor that show it's not: the definition of a leaky abstraction. > All functions take "blocking time" to execute. It's a spectrum of how long you want to wait That's technically correct, but keep in mind that the magnitude difference between your typical REST API call and a CPU instruction is roughly the same as the difference between the size of a football field and the distance to the Sun…
- dboreham 3y agoUpvote from me. I couldn't have written it better myself. Async is a bug not a feature. The only problem with threading (aka CSP aka goroutines aka actors) is scalability to very large numbers of threads. imho it's better to focus on solving that problem than on switching to an unworkable alternative concurrent model.
- nocontextpls 3y ago[dead]
- ghosty141 3y agoThe biggest benefit of async/await is imo. in GUI programming where you simply can't have blocking code a lot of the time, and moving everything between the GUI thread and the backend thread can be rather costly and annoying as well as buggy if people are not 100% aware which thread accesses what. I find it rather odd that you say "easier to reason about.". I find it much harder to keep a mental model in my head which thread currently does what compared to async/await code which you can write like synchronous code. You generally don't have to be that hyper-aware.
- starcraft2wol 3y ago> in GUI programming where you simply can't have blocking code a lot of the time, This can be transparently solved by the GUI library. The main thread does a loop and polls events from a queue. Those events are generated by a gui on another thread. It can be designed so gui itself can be manipulated on the mainpulated on the main thread, and the event handling and rendering is double buffered, or synchronized for you. > easier to reason about The style of code OP is describing looks like `if then else`. You can reason about the state of the system using traditional programming logic.
- jayd16 3y agoWhat GUI system are you referring to? As far as I know, pretty much all major UI frameworks use a single thread, or at best a gui thread and a render thread.
- starcraft2wol 3y agoI believe SDL works how I described. But yes, I was trying to describe a gui/render thread split.
- jayd16 3y agoPulling out the render thread just helps offloads the GPU calls, no? The GUI thread still has the usual single thread concurrency issues most programmers deal with.
- zetaposter 3y agoasync/await is about superimposing useful CPU computations with slow I/O operations. For example, you would read data fragment D0 from a network socket, and right before doing any work on it, you ask the OS to fetch data fragment D1, etc. This would is faster than reading and working in distinct time intervals. And despite whatever you seem to believe, it would also be faster than reading and working using thread parallelism. Because even if you eliminate the synchronization overhead or you devise a good lock-free algorithm, thread context switches still have a massive overhead, not to mention issues related to memory bandwidth and cache coherence. Still, how does one gather the nerve to call a feature present in most modern languages, from C# to Zig, a hype?
- zetaposter 3y agoI should add that the concurrent algorithm I described for processing data from a socket, i.e asynchronously read data fragment (i + 1) then do work on data fragment i, would only be optimal if the throughout of the work you do on the data is higher than reading throughout. Even if the above condition doesn't hold, you would have to be very careful to do better with threads. Is it just an arbitrary design decision that NodeJS is single-threaded?
- bluepod4 3y agoMaybe I’m wrong but the “asynchronous programming started with JavaScript” take doesn’t seem factual.
- cgh 3y agoCorrect, I believe it originated with Microsoft via .Net in the mid-2000s and was picked up by the JS ecosystem much later. The fact that Microsoft had a hand in async/await’s emergence may influence how people feel about it.
- ilyt 3y agoI think that's spot on. More mature model of that is message passing like in Erlang or in a bit more bastardized version, in Golang. And it works there because you can write "normal" code with no colored functions and other baggage that JS-like async brings with it. And it works beautifully on multi-core machines. async is just strict, shitty subset of that where you're limited either by bad implementation (any single-process scripting language) or language limitations (no GC in Rust would make Erlang/Go-like message passing much harder).
- atombender 3y agoThe tradition of async programming goes back much further back than JS. Doing async I/O — usually referred to as event-driven programming — has been a popular technique in C and C++ for decades, with epoll(), kqueue, libevent/libev/libuv, Boost Asio, ASE, and so on. A lot of modern C software is built on async I/O, notably projects like Nginx, Memcached, Tor, Chrome, ntp, Redis, etc.
- munificent 3y ago> has been a popular technique in C and C++ for decades, with epoll(), kqueue, libevent/libev/libuv, Boost Asio, ASE, and so on. JavaScript is older than all of those. (Libuv in particular was harvested from Node, which was itself built on top of JavaScript.) Obviously JS didn't invent callback-based async IO, but I think you're forgetting how old JS is and how relatively new that style of IO is.
- atombender 3y agoThis thread is about async/await, which was added to JavaScript in 2017. Before then, async programming was only done with Node.js (unless you consider windows.setTimeout() to be "event-driven programming"), which came out in 2009. Event-driven programming was an established paradigm years before then.
- munificent 3y ago> This thread is about async/await, which was added to JavaScript in 2017. The article is about async/await, but the comments I'm replying to seem to be about asynchronous programming in general. JS was doing async for many years before async/await was added.
- atombender 3y agoNobody was doing async JS before Node, which came long after async I/O was an established paradigm.
- cconstantine 3y agoI think this is a really important comment, and until about 6mo ago I would have completely agreed with you. I even made these same arguments with my coworkers; it's just cooperative multithreading, it's making up for a defect in js, just use threading primitives. I think some people might use async in a fad-y way when they don't need to, or don't understand what it really is and think of it as an alternative to multithreading. You've generated a lot of good discussion, but maybe having a specific example of where async/await made writing a multithreaded process easier will help. What changed my mind was accidentally making a (shitty/incomplete) async system while implementing a program "The Right Way" using threads and synchronization primitives. The program is for controlling an amateur telescope with a lot of equipment that could change states at any moment with a complex set of responses to those changes depending on what exactly the program is trying to accomplish at the time. Oof, that was a confusing sentence. Let's try again; The telescope has equipment like a camera, mount, guide scope, and focuser that all periodically report back to the computer. The camera might say "here's an image" after an exposure is finished, the mount might say "now we're pointing at this celestial coordinate", the focuser might say "the air temperature is now X", and the guide scope might say "We've had an error in tracking". Those pieces of equipment might say those things in response to a command, or on a fixed period, or just because it feels like it. Controlling a telescope can be described as a set of operations. Some operations are fairly small and well contained, like taking a single long exposure. Some operations are composed of other operations, like taking a sequence of long exposures. Some operations are more like watchdogs that monitor how things are going and issue corrections or modify current operations. When taking a sequence of long exposures the program would need to issue commands to the telescope depending on which of those messages it receives from the telescope or the user; If the tracking error is too high (or the user hits a "cancel" button) we might want to cancel the current exposure. If the air temperature has changed too much we might want to refocus after the currently running exposure is finished. If the telescope moves to a new celestial coordinate we probably want to cancel the exposure sequence entirely. So, how do we manage all that state? The way I solved it was to make a set of channels to push state changes from the telescope or user. Each active operation would be split into multiple methods for each stage of that operation, and they would return an object that held the current progress and what it needed to wait on before we could move onto the next stage. That next stage would be triggered by a controlling central method that listened for all possible state changes (including user input) and dispatch to the next appropriate method for any of the operations currently running. To make things a little simpler I made a common interface for that object that let the controlling central method know what to wait on and what to call next. This allowed me the most control over how different concurrent operations were running while staying completely thread-safe. It was great, I could even listen to multiple channels at the same time when multiple operations were happening concurrently. At this point I realized I'd accidentally made an async system. The central controlling method is the async runtime. The common interface is a Future (in rust, or Promise in js, or Task in C#). Splitting an operation into multiple methods that all return a Future is the "await" keyword. Once I accepted my async/await future, operations that were previously split across multiple methods with custom data structures to record all of the intermediate stages evaporated and became much more clear. I'm still using multiple threads for the problems that benefit from parallel computation, but making use of the async system in rust has made implementing new operations much easier.
- 0x457 3y ago> It didn't start because it is so awesome, it started because JS can't do parallel any other way. That's just not true. It started because starting a thread for connection isn't scalable at all. Asynchronous programming was in use way before nodejs even in languages that have proper threads.
- mschuetz 3y agoI strongly disagree. Async/Await is one of the nicest, cleanest ways to deal with asynchronous tasks, and asynchronous tasks are everywhere. Loading data from disk without blocking and doing something once this is done -> async. Memcpying data from CPU to GPU without blocking -> async. Memcpying data from GPU back to CPU without blocking -> async. Sure, there are other ways to handle these things like polling state, but async makes it trivial and readable. But this mostly applies to JS which has a kickass implementation of async/await. Whatever C++ tried to do, it's an awful mess so there I still use threads with busy loops, polling, etc., whatever makes sense for the task at hand.
- usrbinbash 3y ago> But this mostly applies to JS which has a kickass implementation of async/await. The fact that JS simply has no other options for doing anything concurrently, might have something to do with that.
- mschuetz 3y agoHuh? It had other options longer than it had async/await. Async/await is a fairly recent addition. E.g. before fetch with async/await, there was XmlHTTPRequest with callbacks. It also had Web Workers as a means for parallel&concurrent processing for way longer than it had async/await.
- bob1029 3y agoI feel like there are 2 mostly independent angles to this: Some believe that async makes the code easier to reason with, especially in cases where num_threads << num_sessions. This is a very subjective engineering conversation. You could certainly make strict threads work in any situation with some DIY, but I would be near the front of the line to make an argument to at least start with async/await if there is a paying customer involved. I think handling I/O becomes a joy when you have these abstractions at your disposal. The other angle is performance. If you are reaching for async programming out of the gate because you want to go fast, you are making an epic mistake. Unnecessary context switching between threads will chop multiple orders of magnitude off a single thread best case. Unless you are 900% sure that the cost of communicating between threads is worth the squeeze, you should stick with a single-threaded paradigm (or use async/await responsibly). Now, you may decide that losing performance in order to leverage more "sugary" programming primitives is worthwhile. We certainly make that decision many times over throughout - Interpreted languages, GC, etc. While I could implement our webapp using a socket select server and still easily meet our performance objectives, the complexity of managing this is not worth it. Async/await would still kick my ass in terms of performance because the runtime has been so carefully tuned around it. I can beat it in latency terms, but only for a trivial # of clients.
- marcosdumay 3y ago> it started because JS can't do parallel any other way There are two different subcultures pushing for asynchronous. You are correct, one is based on people learning to program in JS and suffering from either Stockholm syndrome or some other problem that blocks them from thinking in sequential IO. The other grew mostly out of the 10k problem at the late 00's, and is very correct on their assessment in that asynchronous code allows for some high-performing architectures that are much better than anything you can get synchronously. Those two groups are trying to solve different problems, with different architectures, in different parts of the software stack. Rust's async supports both of them, so the discourse is really confusing.
- unscaled 3y agoProper asynchronous programming is not something that's hype. In fact, it's something that is quickly disappearing from most I/O-bound code. Low-level asynchronous I/O (think epoll, kqueue, IOCP) was popularly implemented by many high-performance servers in the early 2000s, often as response to the C10k problems. It's really Nginx, Haproxy, Lighttpd, libevent (and later libev and libuv) which popularized this programming style in systems programming. It's worth noting that during the 1990s, multi-threaded programming did not completely dominate as the model for network servers. I believe it was mostly due to multi-threading support was uneven across the different UNIX flavours of the day, but regardless of the cause, some popular servers (mostly notably Apache httpd) started out as multi-process based, using a pool forks the same way you'd use a thread pool. Other servers were written using the 1990s incarnation of asynchronous programming, essentially using select() or poll() (or WSAWaitForMultipleEvents on windows). From the programmer's perspective, these act mostly the same epoll, but are just less efficient. It is during that time that the C10k problem and its asynchronous solution was experiencing its peak hype cycle that high-level languages got interested in the game, and implemented asynchronous I/O with callbacks. I believe it started with Python and Twisted, but node was the poster-child. OS-level threads were either not supported by the language (Node.js) or severely encumbered by having a GIL (Python). Green threads or coroutines would have probably been a better fit for this languages, but if you're just writing a library or a runtime for a language you don't control, that's harder (of course, gevent in Python went and manage to do that anyway). By the time async/await came to Javascript, this wasn't part of a hype. Javascript has already widely adopted callbacks and then promises as a bottom-up, library oriented solution. Most I/O APIs were promise-based. Even if ES6 added go-like coroutines, all the APIs you had were already accepting a callback or returning a promise. You'd still had to do something like "await(myApi())" every time you're calling that API, not to mention having to introduce synchronization primitives to the language and watching code that never had to care about synchronization before break. Async/await by itself, is not really asynchronous programming. Behind the scenes, it is implemented asynchronously (just the same as I/O in goroutines is!), but the programmer is writing code that looks linear and synchronous. The real trend nowadays is to eschew synchronous I/O and hide the complexity of asynchronous I/O behind synchronous-looking code. Explicitly asynchronous programming (like callbacks or non-awaitable promises) is just as trendy as Ruby on Rails or flip phones, that is - yeah, sure, it was fairly trendy back in the 2005. Nowadays you've got two popular M:N thread models for running multiple synchronous tasks which perform asynchronous I/O behind the scenes: The green thread model (Go, Java's Virtual Threads) and the state machine transformation model (a.k.a. async/await). If you think the async/await model is inferior to the green thread model used by Go, that's a different story. I think each has its own pros and cons, but claiming that only async/await receives hype is untrue. The green threading model receives a fair share of its own hype ("Which color is your function"), and its usually the proponents of the green threading model who claim that their model is strictly superior while the other model has no merit at all, and not otherwise. If you go back to Rust, Rust definitely tried the green threads model, as many people have already said. It had to abandon it. Go is not to be a full-spectrum systems language, and can get along pretty well with being garbage collected and running its own scheduler. Rust has to run on some environments and contexts where you just can't do that. Rust is also very sensitive to overhead introduced by features (that's the entire "zero-cost abstraction" theme), and it does not shy away from adding some complexity in exchange of performance. Otherwise why won't it just do away with lifetimes altogether? While I concur the callbacks hype in JS was probably misguided (although understandable), I find it hard to believe that the decision to use async/await in Rust was based on hype.