8 ms·
There are a lot of moments not covered. For example: - async/await runs in context of one thread, so there is no need for locks or synchronization. Unless one
by adontz 3y ago
There are a lot of moments not covered. For example:
- async/await runs in context of one thread, so there is no need for locks or synchronization. Unless one runs async/await in multiple threads to actually utilize CPU cores, then locks and synchronization are necessary again. This complexity may be hidden in some external code. For example instead of synchronizing access to a single database connection it is much easier to open one database connection per async task. However such approach may affect performance, especially with sqlite and postgres.
- error propagation in async/await is not obvious. Especially when one tries to group up async tasks. Happy eyeballs are a classic example.
- since network I/O was mentioned, backpressure should also be mentioned. CPython implementation of async/await notoriously lacks network backpressure causing some problems.
- graphenus 3y agoAsync/await just like threads is a concurrency mechanism and also always requires locks when accessing the shared memory. Where does your statement come from?
- romanovcode 3y ago> Where does your statement come from? This is how async/await works in Node (which is single-threaded) so most developers think this is how it works in every technology.
- bheadmaster 3y agoEven in Node, if you perform asynchronous operations on a shared resource, you need synchronization mechanisms to prevent interleaving of async functions. There has been more than one occasion when I "fixed" a system in NodeJS just by wrapping some complex async function up in a mutex.
- nurple 3y agoThis lacks quite a bit of nuance. In node you are guaranteed that synchronous code between two awaits will run to completion before another task(that could access your state) from the event loop gets a turn; with multi-threaded concurrency you could be preempted between any two machine instructions. So while you _do_ have to serialize access to shared IO resources, you do _not_ have to serialize access to memory(just add the connection to the hashset, no locks). What you usually see with JS for concurrency of shared IO resources in practice is that they are "owned" by the closure of a flow of async execution and rarely available to other flows. This architecture often obviates the need to lock on the shared resource at all as the natural serialization orchestrated by the string of state machines already naturally accomplishes this. This pattern was even quite common in the CPS style before async/await. For example, one of the first things an app needs do before talking to a DB is to get a connection which is often retrieved by pulling from a pool; acquiring the reservation requires no lock, and by virtue of the connection being exclusively closed over in the async query code, it also needs no locking. When the query is done, the connection can be replaced to the pool sans locking. The place where I found synchronization most useful was in acquiring resources that are unavailable. Interestingly, an async flow waiting on a signal for a shared resource resembles a channel in golang in how it shifts the state and execution to the other flow when a pooled resource is available. All this to say, yeah I'm one of the huge fans of node that finds rust's take on default concurrency painfully over complicated. I really wish there was an event-loop async/await that was able to eschew most of the sync, send, lifetime insanity. While I am very comfortable with locks-required multithreaded concurrency as well, I honestly find little use for it and would much prefer to scale by process than thread to preserve the simplicity of single-threaded IO-bound concurrency.
- bheadmaster 3y ago> So while you _do_ have to serialize access to shared IO resources, you do _not_ have to serialize access to memory Yes, in Node you don't get the usual data races like in C++, but data-structure races can be just as dangerous. E.g. modifying the same array/object from two interleaved async functions was a common source of bugs in the systems I've referred to. Of course, you can always rely on your code being synchronous and thus not needing a lock, but if you're doing anything asynchronous and you want a guarantee that your data will not be mutated from another async function, you need a lock, just like in ordinary threads. One thing I deeply dislike about Node is how it convinces programmers that async/await is special, different from threading, and doesn't need any synchronisation mechanisms because of some Node-specific implementation details. This is fundamentally wrong and teaches wrong practices when it comes to concurrency.
- deleted 3y ago[deleted]
- conradludgate 3y agoIf you perform single threaded async in Rust, you can drop down to the cheap single threaded RefCell rather than the expensive multithreaded Mutex/RwLock
- cogman10 3y agoThat's one example of a lock you might eliminate, but there are plenty of other cases where it's impossible to eliminate locks even while single threaded. Consider, for example, something like this (not real rust, I'm rusty there) lock { a = foo(); b = io(a).await; c = bar(b); } Eliminating this lock is unsafe because a, b, and c are expected to be updated in tandem. If you remove the lock, then by the time you reach c, a and b may have changed under your feet in an unexpected way because of that await.
- josephg 3y agoYeah but this problem goes away entirely if you just don’t await within a critical region like that. I’ve been using nodejs for a decade or so now. Nodejs can also suffer from exactly this problem. In all that time, I think I’ve only reached for a JS locking primitive once.
- cogman10 3y agoThere is no problem here with the critical region. The problem would be removing the critical region because "there's just one thread". This is incorrect code a = foo(); b = io(a).await; c = bar(b); Without the lock, `a` can mutate before `b` is done executing which can mess with whether or not `c` is correct. The problem is if you have 2 independent variables that need to be updated in tandem. Where this might show up. Imagine you have 2 elements on the screen, a span which indicates the contents and a div with the contents. If your code looks like this mySpan.innerText = "Loading ${foo}"; myDiv.innerText = load(foo).await; mySpan.innerText = ""; You now have incorrect code if 2 concurrent loads happen. It could be the original foo, it could be a second foo. There's no way to correctly determine what the content of `myDiv` is from an end user perspective as it depends entirely on what finished last and when. You don't even know if loading is still happening.
- bsder 3y agoI have lots of issues with async/await, but this is my primary beef with async/await: Remember the Gang of Four book "Design Patterns"? It was basically a cookbook on how to work around the deficiencies of (mostly) C++. Yet everybody applied those patterns inside languages that didn't have those deficiencies. Rust can run multiple threads just fine--it's not Javascript. As such, it didn't have to use async/await. It could have tried any of a bunch of different solutions. Rust is a systems language, after all. However, async/await was necessary in order to shove Rust down the throats of the Javascript programmers who didn't know anything else. Quoting without.boats: https://without.boats/blog/why-async-rust/ https://without.boats/blog/why-async-rust/ > I drove at async/await with the diligent fervor of the assumption that Rust’s survival depended on this feature. Whether async/await was even a good fit for Rust technically was of no consequence. Javascript programmers were used to async/await so Rust was going to have async/await so Rust could be jammed down the throats of the Javascript network services programmers--technical consequences be damned.
- seabrookmx 3y agoThreads have a cost. Context switching between them at the kernel level has a cost. There are some workloads that gain performance by multiplexing requests on a thread. Java virtual threads, golang goroutines, and dotnet async/await (which is multi threaded like Rust+tokio) all moved this way for _performance_ reasons not for ergonomic or political ones. It's also worth pointing out that async/await was not originally a JavaScript thing. It's in many languages now but was first introduced in C#. So by your logic Rust introduced it so it could be "jammed down the throats" of all the dotnet devs..
- lelanthran 3y ago> So by your logic Rust introduced it so it could be "jammed down the throats" of all the dotnet devs.. You're missing his point. His point is that the most popular language, which has the most number of programmers forced the hand of Rust devs. His point is not that the first language had this feature, it's that the most programmers used this feature, and that was due to the most popular programming language having this feature.
- dehrmann 3y agoasync can be scarier for locks since a block of code might depend on having exclusive access, and since there wasn't an await, it got it. Once you add an await in the middle, the code breaks. Threading at least makes you codify what actually needs exclusive access. async also signs you up for managing your own thread scheduling. If you have a lot of IO and short CPU-bound code, this can be OK. If you have (or occasionally have) CPU-bound code, you'll find yourself playing scheduler.
- cageface 3y agoYeah once your app gets to be sufficiently complex you will find yourself needing mutexes after all. Async/await makes the easy parts of concurrency easy but the hard parts are still hard.
- junon 3y ago> async/await runs in context of one thread, Not in Rust.
- pohl 3y agoThere is a single thread executor crate you can use for that case if it’s what you desire, FWIW.
- junon 3y agoYes of course, but the async/await semantics are not designed only to be single threaded. Typically promises can be resumed on any executor thread, and the language is designed to reflect that.
- iknowstuff 3y agoThis is completely wrong. You gotta learn about Send and Sync in Rust before you speak. Rust makes no assumptions and is explicitly designed to support both single and multi threaded executors. You can have non-Send Futures.
- junon 3y agoI'm fully aware of this, thanks @iknowstuff. >>>>> Typically promises are designed... I'm merely saying Rust async is not restricted to single threaded like many other languages design their async to be, because most people coming from Node are going to assume async is always single threaded. Most people who write their promise implementations make them Send so they work with Tokio or Async-Std. Relax, my guy. The shitty tone isn't necessary. EDIT: Ah, your entire history is you just arguing with people. Got it.
- specialist 3y ago> backpressure should also be mentioned I ran into this when I joined a team using nodejs. Misc services would just ABEND. Coming from Java, I was surprised by this oversight. It was tough explaining my fix to the team. (They had other great skills, which I didn't have.) > error propagation in async/await is not obvious I'll never use async/await by choice. Solo project, ...maybe. But working with others, using libraries, trying to get everyone on the same page? No way. -- I haven't used (language level) structured concurrency in anger yet, but I'm placing my bets on Java's Loom Project. Best as I can tell, it'll moot the debate.