17 ms·
Reading through comments on async/await-related articles, I wonder if I'm the only person who find the whole concept of async/await utterly weird. Specifically
by divan 7y ago
Reading through comments on async/await-related articles, I wonder if I'm the only person who find the whole concept of async/await utterly weird. Specifically I have troubles embracing the need to mark function as "async" – it just doesn't make sense to me. Synchronous or asynchronous is something up to the caller to the decide – not to the function itself.
Like in real life I can do something synchronously (meaning paying full attention), or I can do the same task asynchronously (watching youtube videos in the background while cooking, for example), but it's still up to me. Being "async" is not a property of the "watching youtube" action, it's the property of the caller running this action.
That's the reason why CSP-based concurrency models really works well for me – it's just so easy to map mental models of system's behaviour from the head directly to the code. You have function/process and it's up to you how do you run it and/or how synchronize with it later.
Async/await concept so popular in modern languages is totally nuts in this aspect. Maybe it's just me, but I find myself adding more accidental complexity to the code just to make async/await functions work nicely, especially for simple cases where I don't need concurrency at all, but one "async" function creeps in.
- inglor 7y agoThat's 'lying' though, async/await is a great way to know which of your functions perform i/o and which do not. Namely in platforms like Node the convention is to not do synchronous I/O and as a communication tool - everything that doesn't use async/await is promised to never block or perform I/O. You might claim it's a lower level of abstraction but it makes concurrency a _lot_ simpler and it's why often when I write go I have to write 35 lines for something that would take me 5 in C#, Python, JavaScript or now Rust.
- divan 7y agoThanks, that's interesting point. I see how it might be useful, but it's still looks as a minor optimization that doesn't outweigh the drawbacks of the model. How do you deal with a situations when you have non-async/await code, and then you need to call just one function, which happened to be "async", and you can't do until you mark all your functions async as well? I find myself in this situation too often with Dart/Flutter, and it's especially embarrassing experience as it happens usually with a simple parts of code, where I don't need concurrency at all (like, get local directory name and store file to it). This "forced refactoring" also hits language design corners like "you can't call async functions" in constructor or `iniState()` and I end up writing async wrappers and other helpers functions just to make the whole thing work. > it makes concurrency a _lot_ simpler I find people put different meaning in the word "simpler". Do you mean that code requires less lines/chars to write or that it's easier to read and understand (those are totally different properties, often conflicting)?
- asark 7y agoI hate Node's model of async concurrency. I need one thing to happen, then another depending on the result of the first, and so on, way more often than I can let several things happen at once or in any order. Probably something like 100x as often. Even when I can have multiple pieces of logic executed at once or in any order, it's usually just a couple, and internally they usually comprise more of the same dependently-ordered execution. If I can really kick off a ton of something at once it's usually one function/operation, which again... consists, internally, of a bunch more stuff that needs to happen in a certain order. It'd be way more convenient if Node just assumed it couldn't carry on with the next operation until the current one was done, even if the current operation is async I/O—feel free to go off and process something else, just no more of my code—unless instructed otherwise. IOW, caller decides, yes. I 100% do not understand the appeal of a system where you have to specify that you do want the next instruction to wait on the result of the previous one. "But you'll block node's single thread during your I/O!" no, it can go do something else, just no more of this particular line of logic in my code unless I specifically tell it otherwise, thanks. Caller decides is both easier to follow and more useful.
- laughinghan 7y agoSounds like you might prefer Erlang or Go. But to be clear, "it'll block Node's single thread!" is not the reason for explicit await. The reason for explicit await is to guarantee that after calling a function foo(), only the state that foo() changes could have changed. Whereas after awaiting on an async function bar(), or any other promise, literally any state-mutating code in the entire application might have run. Obviously plenty of languages have this problem and deal with it, usually with locks like in Go and Java, or by banning shared mutable state entirely and only allowing message-passing for communication between concurrent "lines of logic" like in Erlang. (As far as I understand Go basically tries to get the best of both worlds by strongly encouraging message-passing, with locks intended only for advanced situations like custom concurrent data structures.) But there's no way for Node to change to either of those models without breaking every single nontrivial piece of code ever written in it. And apparently Rust intentionally chose not to go with those models because they require a runtime and make compatibility with C very difficult, well-known advantages over Go.
- michaelmior 7y ago> everything that doesn't use async/await is promised to never block or perform I/O I don't think that's true. See fs.readFileSync for example. You could say that's an exceptional case (which I think it is), but it means that there's no "promise" that functions without async/await don't perform I/O.
- inglor 7y agoRight, there is always an escape hatch, this is also true for haskell with unsafePerformIO - because of practicality. That's a good thing :]
- heavenlyblue 7y agoa) this function is clearly marked as sync b) the question of why does it exist if the async version if available is beyond me
- eropple 7y agorom a practical perspective, it's much easier to deal with `fs.readFileSync` in top-level code than to dive face-first into callbacks. (Remember, `fs.readFile` doesn't return a `Promise`.) If `fs` was promise-aware and top-level await existed, it would be a different conversation. As-is, for writing a one-off script, avoiding callback hell is pretty nice.
- michaelmior 7y agoAgreed. I've used it from time to time for writing simple scripts and it does make things much easier. You don't really have to worry about a CLI script blocking.
- amaranth 7y agoTop-level await is still a problem but a Promise version of the `fs` API has been available since Node v10. https://nodejs.org/api/fs.html#fs_fspromises_readfile_path_options https://nodejs.org/api/fs.html#fs_fspromises_readfile_path_o...
- spinningslate 7y agoI can confirm there are at least 2 of us. I suspect a few more given previous discussions on the topic, e.g. [0]. Steve Klabnick provided some useful context on that thread for the Rust decision specifically. More generally though, you've nailed my primary misgiving: > Synchronous or asynchronous is something up to the caller to the decide – not to the function itself. Erlang/Elixir and Go place the decision with the caller, not the callee. That just seems much more sensible. An alternative is perhaps to see async/await as dataflow going through the awkward teenager stage. Maybe one day we'll wake up and languages in general will have fully embraced dataflow variables as first order constructs. [0] https://news.ycombinator.com/item?id=20400991 https://news.ycombinator.com/item?id=20400991
- woah 7y agoIn go (and anything else with green threads), you pay the price for not putting await in front of stuff by introducing a whole new semantic concept of different threads which must now communicate with each other with other specialized data structures etc. Multithreading is useful when you will actually be using the CPU’s full capacity, but is an enormous extra abstraction if not. With async/await you just have to introduce a little extra syntactical clutter.
- amelius 7y agoCan't a compiler easily spot most cases and convert code from multithreaded to async style under the hood, at least in idiomatic Go?
- jules 7y agoYou need synchronisation primitives with async/await too. If you make every function async then you basically have the same as 1:N green threads. If you have pre-emptive threads or true multithreading with a N:M model then you do need to be more careful with synchronisation, but in principle this is orthogonal to async/await vs green threads.
- mamcx 7y agoCount me as third. I dislike how this stuff "infect" all your code calls: https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ https://journal.stuffwithstuff.com/2015/02/01/what-color-is-... And the problem is rust is that we already have other stuff that infest everything: Borrows, mutability, ownership...
- swsieber 7y agoPerhaps it's because you can easily bolt on CSP to the languages where it's popular? JS is single threaded, and Rust and C# don't have green threads. I consider Rust to be an antithesis to your remarks because it has realoy good tools to prevent concurrency issues and yet still wants it. I think on a deeper level it's because there are times where the thread isn't really doing anything and is instead waiting, and that time could be better apent doing something else. That requires sync, either with callbacks, promises or sync/await.
- Tyr42 7y agoYour thought that it's the caller makes some sense. Think of the OS level call to read a file, you can either ask for it to block until end of line, or just give you what it has now, based on a flag. But "wait until there's eol, than call this callback" or its cousin the future which is "when complete, read here to find the callback(s) to call" which let's you fill in the callback after the call, that's inherently an async call. Same way sending a letter forces you to wait. Sure you could stand by the mailbox until the reply arrives, but that's just converting the async call to synchronous by waiting. Though I don't really see how futures are that different from a cps, especially if you view them as chaining .then calls, which are exactly continuations. Await just let's you drop some syntax, and unify scopes.
- PJDK 7y agoThing of it as a different analogy. There are various chores to do around the house - some of them must be done synchronously (do the ironing - we don't want to leave the iron on). Some of them can be done asynchronously (run the washing machine). If you want to write out your tasks it might look something like this. LoadWasher() (sync) washerRunning = RunWasher() (async) var cleaning = CleanKitchen() (async) wetClothes = await washerRunning; LoadDryer(wetClothes) clothesToIron = await RunDryer() // while we're awaiting we walk back up the stack and can continue cleaning the kitchen until the dryer is finished IronClothes(clothesToIron) // this is just happening synchronously await cleaning //last job to do so we need to make sure we finish! Don't know if that helps, but I think it's an interesting analogy
- spinningslate 7y agoThat is a good example. It illustrates that async is a partial abstraction on the way to dataflow. It's not immediately obvious from your example what's really going on: whereas re-writing it as dataflow pipelines makes the intent clear: LoadWasher() | RunWasher() | LoadDryer() | RunDryer() | IronClothes() CleanKitchen() where `|` is piping the output of one operation into the next one (as per *nix shell). The two lines are independent and so can be scheduled according to resources available. Doing so makes the inherent dependencies trivially clear. It doesn't matter which are "long" running and which aren't: it's not possible to run the washer until it's been loaded. Using async/await conflates two things: 1. What's "long" running, for some definition of "long" (--> make it async) 2. What's dependent on what (await) -- EDIT: fix formatting & added missing "RunDryer()" step
- dmix 7y agoSo you basically you'd have to rely on each of the function signatures, probably with some text editor/IDE by hovering over them, to figure out which one in the pipe list is Async? Or jump to each of them with ctags. Then wrap it in a try/catch or pattern match the result to handle the various errors different errors...
- 7y ago
- zzzcpan 7y agoIf you don't have async markings, you can't know where in your code you have concurrency and this would require you to use synchronization primitives. For shared memory languages this means all that error-prone shared memory multithreading, like in Go [1]. [1] https://songlh.github.io/paper/go-study.pdf https://songlh.github.io/paper/go-study.pdf
- atombender 7y agoBut a synchronous Rust function can still do threading, which makes your point moot.
- zzzcpan 7y agoIt's not moot, there is no point to do threading from within asynchronous programming primitives.
- atombender 7y agoYour original comment implies that "async"-marked functions are the only functions that have concurrency ("If you don't have async markings, you can't know where in your code you have concurrency"), which isn't true. You can have threading in any function. So marking a function async makes no difference.
- zzzcpan 7y agoNo, think of it as a convention or a framework. While it's ok to wait for asynchronous code to finish in your synchronous code, it's not ok to do threading from within your asynchronous primitives, so you wouldn't need threading synchronization primitives.
- bryanlarsen 7y agoLeaving it up to the caller to decide seems reasonable, but you only have control over the top level, you still have no control over how the function you're calling calls other functions. Also, blocking on a future is trivial (just call poll). So I would argue that async/await does a better job of leaving it up to the caller to decide: use await if you want async, use poll if you want sync.
- Jweb_Guru 7y agoYou can run an async routine synchronously if you use the correct runtime, or block while polling until the function succeeds... "async" functions are functions that can be suspended and turned into an opaque thunk that you can pass around as Rust data, and "synchronous" functions aren't. If you like, you can think of async functions as being functions that can run either synchronously or asynchronously. You can't (in safe Rust) easily replicate what async does for you because it understands how to handle borrows across yield points, which turn into self borrows when you turn the stack into a concrete object in Rust. That's not really an issue in garbage collected languages so it's not quite as much of a necessity to have a specific keyword, but it's still inconvenient in most languages to write everything in CPS or use combinators to acquire thunks. As for why you would want a function that's not asynchronous... well, various reasons, but one is performance. Creating a thunk and then sending it to a runtime which decides what to do with it (or polling) usually has some overhead compared to linearly calling a function on the stack. Another reason is that, in Rust, top-level asynchronous functions normally need to be quite conservative with what they own if you want to use them with an actual asynchronous runtime--many of them like to send the thunks between threads in a static thread pool, which limits you to thread safe constructs and owned (or static-borrowed) data. As a result, even if there was no overhead for using an asynchronous function synchronously, you would still in practice have to either sacrifice performance and generality by keeping your data owned and thread-safe (to make things work with thread pools), or embrace all the usual stuff you would do in synchronous code (like borrow things from a previous stack frame) and lose the ability to use your async function at the top level in most existing asynchronous runtimes. So from that standpoint, it's not really up to the caller. This is again not really an issue in garbage collected languages that only allow references to managed, heap-allocated objects. There are more reasons than that (being able to efficiently call out to blocking C APIs, wanting to use builtin thread locals across function calls without worrying about how the function call is going to be handled, current issues with recursion, etc.). They may be considered artifacts of the implementation or otherwise resolvable--I'm not necessarily saying they aren't--but they are reasons in practice why you want to have synchronous functions available. So tl;dr I think most people's immediate intuitions about how async/await should just be sugar, or async should be up to the caller (i.e. all functions should be async), don't straightforwardly apply to Rust even if they are valid for most other languages.
- onebot 7y agoI am a huge Rust fan, but couldn't really articulate why async/await seemed so complicated. You have nailed it right on the head! This is a exactly how I feel as well and I write Rust code every day. But no matter, I will suck it up. The language is evolving and I think async/await has, seemingly, been one of the biggest divisive feature the community has faced. I will still use as there are so many other benefits this language brings.
- coldtea 7y ago>I am a huge Rust fan, but couldn't really articulate why async/await seemed so complicated. Perhaps because you hadn't had the displeasure to work with callbacks, or promises? Which async/await greatly simplifies.
- divan 7y ago> Which async/await greatly simplifies. That's probably the real reason why so many people are so happy and vocal about "simplicity" of async/await – after experiencing concurrency only with callbacks in JS, async/await looks like a blessing.
- rubber_duck 7y agoThreaded IO is a low level thing there because of overhead caused by OS threads and Rust is a low level language with abstractions to deal with such low level interfaces. Async/await is better than callback passing - looks like normal call flow - but you need to know what's happening underneath. Zero cost abstractions. If you understand the implementation details it makes perfect sense - otherwise you can just keep doing the old way (callback pyramids or blocking IO)
- Someone 7y agoYou’re not the only one. See https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ https://journal.stuffwithstuff.com/2015/02/01/what-color-is-..., discussed in https://news.ycombinator.com/item?id=8984648 https://news.ycombinator.com/item?id=8984648
- bryanlarsen 7y agoThat's a Javascript rant from 2015, and almost completely unapplicable to Rust. The rust designers were well aware of the disadvantages of various async implementations and have learned some lessons. First he starts with a rant about the color of a function. In that sense, all typed languages have a color, aka the type of their return parameters. Sure, there a advantages to dynamic languages, but they're going out of fashion now for some good reasons. Then he lists 5 more specific drawbacks of async code in 2015 Javascript 2015. All 5 aren't issues in Rust. #1. Every function has a color: that's how typed languages work #2. The way you call a function depends on its color: the rust compiler doesn't let you get this wrong and provides cut and paste corrections when you do #3. You can only call a red function from within another red function: Use Future#poll #4. Red functions are more painful to call: This is in reference to callback syntax in old node.js code and is fixed by the await sugar even in modern Javascript #5. Some core library functions are red: The clause "that we are unable to write ourselves" is untrue in Rust; you should never have a need to drop down into C to write anything. And this is really just a variant of #3; if it's trivial to convert async to sync this wouldn't be an issue.
- naasking 7y ago> Synchronous or asynchronous is something up to the caller to the decide – not to the function itself. async/await aren't strictly necessary, but to avoid them you need one of: 1. a sufficiently smart compiler with whole program compilation, or 2. to compile two versions of every function, an async variant and a sync variant, or 3. every async wait captures the whole stack, thus wasting a lot of memory. Async/await is basically a new sort of calling convention, where the program doesn't run in direct style but in continuation-passing style. This permits massive concurrency scaling with little memory overhead, but there other tradeoffs as per above. Async/await makes perfect sense for Rust which wants to provide zero-overhead abstractions.
- gpderetta 7y ago4. Pass the incoming continuation as an explicit parameter to the async function and, as long as it never escapes the function (which is equivalent to the async keyword case) or (recursively[1]) any inline child function, the CPS transformation can be performed without full whole program analysis. If the continuation escapes beyond the ability of the compiler to track it, fall back to 3. Annotations can be used to guarantee the 0 overhead optimization. [1] true non-tail recursion is a problem of course, but it is ok for the compiler to give up if it can't statically prove that the recursion depth is less than a documented maximum. edit: rewording
- naasking 7y agoIt sounds like you're suggesting programming in explicit CPS/callback style, which is doable, but doesn't match the goal of the initial post which was to program in direct style and let the callee determine whether it should execute async or sync.
- a1369209993 7y agoI think they're suggesting programming in implicit CPS/callback style. Eg: int bar(int i) { return i+1; } /* bar takes a implicit return address in [esp+0] */ noreturn foo((*ra)(int),int x) { /* where (*)(RetType) is a return address */ int y = bar(x); /* x86 call pushes eip */ ra return bar(y); /* pseudo-tail-call (push ra) */ } int quux(int x) { (*ret)(int); ret = (return); /* get implicit RA */ foo(ret,x); } // unreachable You'd also need to adjust the stack and/or frame pointer though, which is probably going to be the hard part.
- nnq 7y agoWell, practically it is for the caller to decide, you can `await` on a function (use it as if it were sync), or don't, grab it's `Future<something>` result and work with that (async). Now `async`, you could see it as an implementation detail that you need in order to get much better performance...
- brianguertin 7y agoYou aren't the only one. In Kotlin, it's possible to use async/await without modifying your function signatures. Like this: val result = runBlocking { val one = async { doSomethingUsefulOne() } val two = async { doSomethingUsefulTwo() } one.await() + two.await() } https://kotlinlang.org/docs/reference/coroutines/composing-suspending-functions.html https://kotlinlang.org/docs/reference/coroutines/composing-s... EDIT: actually this was wishful thinking, since both functions would block the main thread. The functions need to be modified to "suspend" for this to be truly async. My ideal language would be something like Kotlin suspend semantics, except ALL functions were implicitly "suspend" functions. If that's even possible...
- shepmaster 7y agoDitto in Rust. let result = executor::block_on({ let one = async { do_something_useful_one() }; let two = async { do_something_useful_two() }; one.await + two.await }); See examples in: - What is the purpose of async/await in Rust? — https://stackoverflow.com/a/52835926/155423 https://stackoverflow.com/a/52835926/155423 And why this might be a bad idea in: - What is the best approach to encapsulate blocking I/O in future-rs? — https://stackoverflow.com/q/41932137/155423 https://stackoverflow.com/q/41932137/155423
- brianguertin 7y agoOh cool. And does that actually work? That's great. And the syntax is surprisingly similar, especially considering we are supposedly comparing a low level language to a high level one.
- shepmaster 7y agoYes, one big push in Rust is that you get to have your high-level syntax with your low-level performance. See the sibling comment [1] where I actually tested and ran the code, finally. ;-). I was unaware that Kotlin would race two `async` blocks, so we had to use the `join` combinator here. [1]: https://news.ycombinator.com/item?id=20682191 https://news.ycombinator.com/item?id=20682191
- devit 7y agoNo, a synchronous procedure is a procedure whose minimum completion time depends solely on the speed of the CPU and memory system (and lock contention, in some models), while an asynchronous procedure is a procedure whose completion time also depends on external events. In other words, synchronous procedures are essentially locally CPU-bound, while asynchronous procedures are bound by I/O, network or IO/network/CPU of remote servers. Async functions are the most efficient way to support asynchronous procedures, while non-blocking non-async functions are the most efficient way to support synchronous procedures. There are also adapters to turn synchronous function into asynchronous ones, and to turn asynchronous functions into blocking non-async functions, although they should only be used when unavoidable, especiallly the latter.
- sagichmal 7y agoThis is an idiosyncratic (most would say incorrect) definition of sync and async.
- sansnomme 7y agoYou may enjoy this: https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ https://journal.stuffwithstuff.com/2015/02/01/what-color-is-...
- bryanlarsen 7y agoThat's a rant about callbacks in old style node.js and is completely unapplicable to Rust. More: https://news.ycombinator.com/item?id=20676641 https://news.ycombinator.com/item?id=20676641
- atombender 7y agoI agree. I raised the topic recently on HN here [1], but didn't get much of a response. One of the problems with async is that it's viral. Make a function async and your entire call graph has to be made async, unless they handle the future manually without await. Async introduces a new "colour" to the language. I absolutely agree that asynchronicity should be the provenance of the caller. If you look at typical code in languages with async/await such as JavaScript and C#, awaiting is the common case. So if async causes your code to be littered with awaits anyway, it makes more sense to make awaiting the default (just like it's the default for the rest of the language) and "deferring" the exception. Call it "defer" or something. (As an aside, I was always disappointed that Go's "go" doesn't return a future, or that indeed there's no "future" support in the standard library. Instead you have to muck about with WaitGroup and ErrGroup and channels, which introduces sequencing even in places that don't need it. Sometimes you just want to spawn N goroutines and then collect all their results in whatever order, and short-circuit then if one of them fails. The inability to forcibly terminate goroutines is another wart here, requiring Context cancellation and careful context management to get right.) [1] https://news.ycombinator.com/item?id=20405124 https://news.ycombinator.com/item?id=20405124
- fluffything 7y ago> One of the problems with async is that it's viral. Make a function async and your entire call graph has to be made async In most languages, including Rust, async functions can call synchronous functions, so this isn't entirely true. > unless they handle the future manually without await. At some point, the futures are always handled manually, e.g., via the run-time, or in Rust case, by scheduling on an Executor, which is nothing but a fancy name for a library that manually handles a lot of futures. > If you look at typical code in languages with async/await such as JavaScript and C#, awaiting is the common case. Do you have any example of code that uses await more than synchronous functions ? > So if async causes your code to be littered with awaits anyway, it makes more sense to make awaiting the default (just like it's the default for the rest of the language) and "deferring" the exception. Call it "defer" or something. If the purpose is just to change the defaults, then you'd need `defer vec.len()` to access the length of a vector, or `defer array[i]` to index into an array, or `defer obj.foo` to access the field `foo` of an object, ... unless you want to make all of those operations `async`. If you make await the default and add a `defer` operation that doesn't awaits, then it's hard to imagine how the resulting code could contain less `defer` annotations than `await` ones. At that point you might choose one of the many non-zero-cost solutions to removing the async distinction, and call it a day like Go does.
- Rusky 7y agoThis is all just down to a poor choice of words, and maybe syntax. The choice of `async`-vs-not is not the one you describe. What we label with `async` is a particular compilation style that makes the function interruptible in userspace, in exchange for making synchronous and recursive execution a bit more complicated. This style is important because it gives you event loop-style concurrency without the performance costs of threads (kernel or userspace/green/etc). However, this compilation style still leaves the choice you describe up to the caller. Most languages seem to reverse the default choice syntactically, but you can still synchronously block on a call to an async function or run it concurrently with something else. It just so happens to be useful primarily for doing the latter, so it gets lumped in with it. (And we get unfortunate misunderstandings like "What Color is Your Function?" that miss this point.) As an example of a language that doesn't reverse the defaults, look at Kotlin's `suspend fun`s. While they still have `async`-like callee annotations to control the compilation style, a simple function call behaves the same regardless of the callee, and you instead use various `spawn` or CSP-like APIs to get concurrency. If changing the keyword and making the defaults match threads/CSP isn't enough, perhaps viewing the annotation as part of an effect system would help? The thing being tracked here is "this function can suspend itself mid-execution," and a good effect system even functions be polymorphic over things like this. For example, an effect-polymorphic function passed a closure or interface can automatically take on the effects of its callees, simplifying the program somewhat if you're faced with viral async-ness.
- deepGem 7y agoI totally agree with you, however the alternatives to Async/Await are just way more wierder IMHO. Promises, Futures and what not. I am not very familiar with CSP based concurrency but Async-await strikes a good compromise to me. I just can't deal with this Promise/Future/Callback hell when it comes to managing multiple asyncrhonous or dependent asysnchronous calls. I think Chris Lattner does a way better job at explaining this - https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9f782 https://gist.github.com/lattner/31ed37682ef1576b16bca1432ea9...
- masklinn 7y ago> I totally agree with you, however the alternatives to Async/Await are just way more wierder IMHO. It’s not, however you do need the entire runtime to be in on it. That’s what Erlang, Go or gevent (for python) do: normal concurrency is in user land and normal IO uses « async ». The code itself looks / is synchronous, it just multiplexes in userland before doing so on OS threads.
- divan 7y ago> I am not very familiar with CSP based concurrency I strongly recommend to check it out. I was absolutely mind-blown when saw this talk (without prior knowledge about Go), and that changed my life quite literally. https://www.youtube.com/watch?v=QDDwwePbDtw https://www.youtube.com/watch?v=QDDwwePbDtw I did a few concurrency related projects (see "Visualizing concurrency in Go" https://divan.dev/posts/go_concurrency_visualize/ https://divan.dev/posts/go_concurrency_visualize/, for example), and it's way more easier to reason about and work with than any other concurrency approaches I've seen so far.
- pron 7y agoI completely agree. In Java we're taking a different approach: https://youtu.be/r6P0_FDr53Q https://youtu.be/r6P0_FDr53Q (my talk also explains why this option may not be available to C/C++/Rust)
- nullwasamistake 7y agoFibers are way better than async. Go has them and Java is working on it with project loom. It's a slight performance disadvantage (maybe 2%) but light-years easier to work with. Languages with fibers figure ou execution suspend points and physical core assignment for you and abstract it all away. So physically they're doing the same thing as async, just without all the cruft. Rust decided not to go with fibers to avoid having a runtime. I still disagree with this because they already do reference counting, not every worthwhile abstraction is zero cost.
- steveklabnik 7y agoThe difference with reference counting is that you only get it when you actually use it, but a runtime included by default does not have this property. And having an "opt-out runtime" doesn't fit the bill either; we tried that too. (I'd also be very skeptical of the 2% figure; where did you get that from?) Remember, "zero cost" means "zero additional cost", everything has a cost.
- nullwasamistake 7y ago2% is from Quasar, a Java fiber library. Some simple benchmark compared it to async code. Now that rust has async, couldn't they make fibers an opt-in replacement for threads? Your libraries use async, but you can handle those calls with fibers. Fiber support is compiled into your code but not the libraries
- steveklabnik 7y agoInteresting, I wonder what the details look like. We saw fairly big differences in Rust. > Your libraries use async, but you can handle those calls with fibers. Fiber support is compiled into your code but not the libraries We tried this! Making the attempt is actually one of the biggest reasons that we decided to remove green threads from Rust; it brought in tons of disadvantages and no real advantages.
- nullwasamistake 7y ago
- lenkite 7y agoWhole-heartedly agree. I strongly believe that async/await will be acknowledged as a design-mistake a decade from now. Async should be caller and not callee controlled - it also makes the implementation simpler and reduces maintenance.
- zzzcpan 7y agoIt's been many years already and async/await has proven itself to be a viable feature for shared memory languages that want to be as good at concurrency as they can. Next step is literally dropping shared memory as there is pretty much nothing that can be done better on top of shared memory than syntactically sugared higher-order asynchronous programming primitives.
- youeseh 7y agoI'd rather mark a function as "async" than to constantly do the mental gymnastics of whether a function is going to run synchronously or asynchronously, or even worse, always assuming that it'll run asynchronously and always have to deal with callbacks / triggers and the like just to get things to execute in order.
- eridius 7y agoIf I call you on the phone, I expect a synchronous response. If I text you, I expect an asynchronous response. It's ok if you get back to me 3 hours later (well, context-dependent). If I call you, ask you a question, and then you wait 3 hours before giving me a response, I've been sitting there blocked for 3 hours, unable to get any more work done, because I asked for a synchronous response and got an asynchronous one. Even worse, if you try and give me the response over text, I won't see it because I have the phone pressed to my ear and now I'm blocked forever. --- The caller and callee have to agree on whether the communication is synchronous or asynchronous. Asking for synchronous and getting asynchronous doesn't work. Asking for asynchronous and getting synchronous kind of works, but that's not truly synchronous, that's just asynchronous with zero delay before getting the response. Or even worse, asking for asynchronous (and giving you a completion handler to fire) and you fire it synchronously before returning control to me, that way lies madness. Don't do that.
- pcwalton 7y agoAs I've always said, you can use CSP in Rust. Just use threads and channels. They work great, and they even scale well on Linux to all but the absolute most demanding workloads. If you want a Go-like M:N scheduler, we've got that too: use mioco. CSP as implemented in languages like Go is just threads, but with an idiosyncratic implementation.
- ngrilly 7y ago> they even scale well on Linux to all but the absolute most demanding workloads What about the stack size? I guess the default is about 4 MB, compared to a few kB in Erlang or Rust. This implies a virtual memory usage 100x or 1000x higher. I understand the unused virtual pages in each stack will never be mapped to physical memory, but isn't this putting a lot of pressure on the virtual memory system? I'd be curious to read a real world benchmark on this topic.
- mikedilger 7y agoI think of async to mean "When you call me I won't actually do anything right now, I'll just prepare the data structures to help remember what needs to get done, and return those structures to you, which you can then hand over to an event loop (executor) at your leisure". That return type? That's not actually what is getting returned right now when you call the function... that's what the future returned will eventually resolve to. That level of unexpectedness/magic is hard for me to stomach but in this case I'm ok because the feature provided and the ergonomics are so very good. I think of await to mean "When this code is combined with all the other code that needs to be executed asynchronously on the event loop, this code will not be able to progress until this thing we are waiting on finishes. The event loop can therefore use this hint to schedule things and keep the processors busy." I think I built up a good understanding of how this kind of a system works with futures 0.1 and tokio. It took some time but it all clicked together for me. But as for async/await as language features I'm satisfied to just let it be magic. I don't care to look under the covers at this point. I know it's similar to how futures worked, and I trust the rust team weaved it together well.
- Matthias247 7y ago> Synchronous or asynchronous is something up to the caller to the decide – not to the function itself. This doesn't work with Rusts implementation, because the functions have slightly different semantics and limitations. E.g. an async function can't be recursive, since that would lead to a Future type of infinite size. Async functions also don't necessarily run to completion like normal functions. They can instead also return at any .await point, which are implicit cancellation points. That means if someone just adds an async modifier to a function and some .await calls to methods inside it the result might not be correct anymore, since not all code in the function is guaranteed to be run anymore. The cancellation mechanism was an explicit choice of the design - it would probably have been possible too to make async functions always run to completion and to thereby avoid the semantic difference.