7 ms·
What I don't understand is why these functions must be declared async at the function declaration site. Why not allow the programmer to run any expression async
by bruce343434 5y ago
What I don't understand is why these functions must be declared async at the function declaration site. Why not allow the programmer to run any expression asynchronously, which wraps the return value of the expression in a future or promise or what have you?
let promise = async {something+someother();};
// other computations
let result = await promise;
You'd essentially be creating a very ergonomical shortcut to fork-join multithreading which is infinitely more powerful and expressive than whatever this "async function" stuff is.
ETA: perhaps it would be a good idea to have a thread-unsafe keyword, though I'm not sure if rust needs that since rust has a really stringent resource pointer/reference aliasing guarantee.
ETA2: I misunderstood async semantics for rust, it's not a fork/join but send/poll. In any case, I still think you should be able to declare async at the call site, not the function site. That doesn't seem to clash.
- wizzwizz4 5y ago> You'd essentially be creating a very ergonomical shortcut to fork-join multithreading which is infinitely more powerful and expressive than whatever this "async function" stuff is. But that would require starting other threads, which requires everything to be `Send + Sync`, has the overhead of multithreading, and can't be managed with any sort of scheduler (without platform-specific hacks). Unless you use green threads, and that's discussed in the article.
- bruce343434 5y agoSo what then is async? If there is no scheduling/preemption in executing an "async" function, it's not really "async", but rather synchronous. Is async just a new way to say "instruction reordering"? Or is async really just "*defer* until the last possible moment: await"? edit to match yours: not sure what a "green" thread is. In any case, if threads are not an option, an async may always be ignorable, after all, a program may not rely on the order of the execution of its threads/its preemption; that would be a race condition. Textbook definition. As I see it, the fork/join model is very useful for "shingling" code paths that don't have to happen in sequence, but _could_ happen in parallel. Final edit: nothing is otherwise stopping the implementer of async to add a pre-started threadpool to the runtime, with whatever configurable parameters one would want. Post final edit: I think fork/join vs send/sync are just implementation details. But I respect Rust for going with send/sync, just that it doesn't change all that much in terms of the programming interface.
- AndrewDucker 5y agoAsync is running many tasks on many less threads, using polling. See https://rust-lang.github.io/async-book/01_getting_started/02_why_async.html https://rust-lang.github.io/async-book/01_getting_started/02...
- steveklabnik 5y ago(but note that this doesn't mean busy-loop polling, while that will be used in "my first executor" tutorials, production executors don't call a future until it's ready to have more progress made via an event, often from an epoll/kqueue/iocp or similar mechanism.)
- bruce343434 5y agothis sounds like a lot of overhead, though
- DenseComet 5y agoRather, Rust just makes this overhead explicit. Go, Node, etc do the same thing, but just hide what actually happens.
- Groxx 5y agoIt is, relative to not doing it. But it's quite a lot less overhead than a native thread.
- chmod775 5y agoAs opposed to what?
- monocasa 5y agoIt's "compile this as a state machine such that when other futures are waited on internally by this function, store stack state in an anonymous struct that implements Future + Poll, and allow repumping when that Poll implemention says it's ready. This allows you to reuse the same stack when this async function is blocked"
- jayd16 5y agoWrapping sync in async is usually not so hard but usually an anti-pattern. In C# for example, if that synchronous work is long running, it might consume a thread in the shared thread pool and have side effects (starvation) elsewhere in the application.
- bruce343434 5y agoI don't understand what you mean. What is to say that an async-fn is not long running? It can just as well consume a thread, right?
- lijogdfljk 5y agoI may not follow you - but to expand what _(i believe)_ the GP comment said; any long running code - say a loop that spins for an hour, is bad in the async context, because that async thread is part of the runtime. Better to take those special cases and let the executor move the computation to a dedicated thread. At my work i mix a lot of compute heavy work in an async context, and if i let this compute heavy stuff run on my async threads i'd potentially starve my runtime. Tasks wouldn't get woken up on time, etc.
- bruce343434 5y agoright, but this isn't an argument against allowing arbitrary async expressions. I can make an async-fn that runs an infinite loop just as easy.
- lijogdfljk 5y agoThat's the point though - anything blocking the async thread is bad. If your loop is going to run for an hour, you'd put arbitrary yield points in it so that the executor can reliably call other tasks
- jayd16 5y agoWell I can only speak to C# really but... The preferred pattern is to avoid long running tasks in the default thread pool as it's a shared resource. Long running tasks should be executed somewhere else, either some new thread pool or just a new thread or just don't use async and make it your caller's problem. The point is that you don't want to consume the limited shared resource. In your case, what would happen? Would a new thread be created? Is it ok to use the default pool? Did the synchronous method writer even consider this? It's hard to say. Now, does this need to be enforced by the language? No. Developers could simply append "_Async" to make it clear a method can be put on the default thread pool or not. That's still a form a coloring though. Even if you consider it a choice by the caller, it's still a color that needs to be determined and you never want to exhaust your default thread pool. That said, there are other reasons why calling an async method should have different semantics than calling a normal sync method. Just saying that the "can use shared threads" aspect of async methods can probably live as a convention.
- deleted 5y ago[deleted]
- throw10920 5y ago> I still think you should be able to declare async at the call site, not the function site This is the big point. I'd love to see anyone put forth a concrete, valid reason why this couldn't be implemented, because as it stands, it appears that the function coloring problem is exclusively due to poor language design, rather than a technical limitation (where an example of a technical limitation would be escape analysis, which is hard).
- henrydark 5y agoI think the CPS attempt in Nim could do this https://github.com/nim-works/cps https://github.com/nim-works/cps
- jayd16 5y agoExplicit await and promise semantics are useful incase you want to, say, start multiple async calls instead of awaiting one and then the other. But ok, lets say you just want await to be implicit in some cases... Do you actually want this? Currently, you can assume that a function will not change threads mid execution, and that that thread will also not be used to execute other things. Implicit yielding breaks that assumption. Cooperative multithreading becomes impossible because it's much hard to reason about when a method call will yield execution or not.
- throw10920 5y agoI wasn't proposing implicit async/await. I was proposing declaring those at the call site, instead of per function declaration.
- Rusky 5y agoIronically escape analysis is the one thing Rust went above and beyond to solve in a novel way.
- throw10920 5y agoNo, Rust did not solve escape analysis - it was designed to constrain valid programs to those for which escape analysis is trivial (as evidenced by unsafe Rust, which doesn't have those constraints, and yet has nothing else enabling escape analysis). That's like saying that "C solved type inference" - no, it just constrained you to write your types by hand, and then did some checking of them afterward.
- remram 5y agoThis is possible pretty much with the syntax you wrote: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=da3b001b18caae2c91eaf4503c767d5b https://play.rust-lang.org/?version=stable&mode=debug&editio... Of course you need a runtime to drive that future forward, so it's more: let result = runtime.block_on(future); [edit: or if main() is async, I'm not sure if that's what you want to avoid: `let result = future.await;` https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=715b7fe32cf73132803237899d65afcc https://play.rust-lang.org/?version=stable&mode=debug&editio...]
- Rusky 5y agoThe reason to mark a function as async (in today's Rust) is that you want to `await` something within it. If a function does this, it must be async- there's no other way to implement it. If a function does not do this, there is no reason to make it async in the first place- it will never suspend. (Things get uglier when it may or may not need to await something, e.g. if it accepts a closure parameter. Rust doesn't really handle this today, but one approach would be to make the function polymorphic over async-ness- this is similar to but not really the same as what it sounds like you're talking about.) Async functions are for language-level cooperative multitasking, not concurrency per se. The difference between fork/join (what you imagined async to be) and what it actually is, is more than an implementation detail- it's a semantic change to how the program behaves. If you want to make something happen in the background at the call site, there are already ways to do that, they're just not spelled "async fn."
- ridiculous_fish 5y agoBut what's the difference between an async function returning int, and a function returning `impl Future<int>`? If there's no difference why do we need the async keyword? Other parts of the language are conditioned on the return type. For example the ? operator only works if the return type is Option or Result. It seems like `await` could inspect the return type in the same way.
- kibwen 5y ago> But what's the difference between an async function returning int, and a function returning `impl Future<int>`? If there's no difference why do we need the async keyword? No difference, but while the feature was being explored early users found it simpler if they didn't need to think about lifetimes on the returned future. Sometimes sugar is just sugar; same reason that `for` loops exist instead of just using `while`.
- steveklabnik 5y agoYou can't await inside of a function returning impl Future<Int>. (You can inside an async block inside one, but there you go with language-level support again.) You need the collaboration with the compiler to make it all work well. And that means keywords and language-level infrastructure. We tried no sugar, and it was rough: http://aturon.github.io/tech/2018/04/24/async-borrowing/ http://aturon.github.io/tech/2018/04/24/async-borrowing/ (And a proc macro didn't work out either)
- ridiculous_fish 5y agoThe answer is actually pretty dumb: functions are declared async at the declaration site because that's how C# supported legacy code. Introducing a new keyword is a breaking change because existing code may have a variable with the same name. So `await` is a conditional keyword in C#, conditioned on whether the function is marked `async`. And other languages have since just copied that choice. https://dotnetfiddle.net/JUE0Sy https://dotnetfiddle.net/JUE0Sy
- kibwen 5y agoThat may have been C#'s rationale, but it wasn't Rust's rationale. Rust doesn't even strictly require the `async` keyword in a signature to define functions that work with `await`; `await` works by polling a future, and `async fn` is just sugar for defining a function that returns a future. In fact Rust does precisely what the parent suggests, and also offers `async {}` blocks as sugar for defining inline futures: fn bar() -> impl Future<Output = u8> { async { let x: u8 = foo().await; x + 5 } }
- ridiculous_fish 5y agoNeat, I didn't know about the `async {}` feature. I think it's obvious that Rust chose `async fn` for familiarity with other languages. As you say `async` is just sugar on the return type, so logically it should decorate that type, not the fn keyword. "async fn" is even misleading since the function itself returns synchronously (and immediately): it's the returned value where the async-ness lies. So I think Rust's use and placement of `async` is for familiarity, which traces back to C#'s original choice. Not to say that's a bad rationale. Admittedly I didn't follow any of the Rust async design discussions so I could be completely wrong about all of it.
- Rusky 5y ago> "async fn" is even misleading since the function itself returns synchronously (and immediately): it's the returned value where the async-ness lies. You've got the causality backwards- it's the async-ness that causes it to return immediately. Async is primarily about the execution model of the entire function, and the return type is only changed in service of that model. The course of the design discussions started without async or await, using library-based Future combinators. Async was introduced as a way to desugar control flow and move knowledge of Futures into the compiler for borrow checking reasons, not out of any intention to mimic C#.