11 ms·
Isn't it kind of a poor design choice that Rust will not actually begin execution of the function until `.await` is called? If I didn't want to execute the func
by ralusek 7y ago
Isn't it kind of a poor design choice that Rust will not actually begin execution of the function until `.await` is called? If I didn't want to execute the function yet, I wouldn't have invoked it. Awaiting is a completely different concept than invoking, why overload it?
If you want to defer execution of a promise until you await it, you can always do that, but this paradigm forces you to do that. The problem is then, how do I do parallel execution of asynchronous tasks?
In JavaScript I could do
const results = await Promise.all([
asyncTaskA(),
asyncTaskB(),
asyncTaskC()
]);
and those will execute simultaneously and await all results.
And that's me deferring execution to the point that I'd like to await it, but in JavaScript you could additionally do
const results = await Promise.all([
alreadyExecutingPromiseA,
alreadyExecutingPromiseB,
alreadyExecutingPromiseC
]);
Where I pass in the actual promises which have returned from having called the functions at some point previously.
So how is parallel execution handled in Rust?
- Buttons840 7y agoAre you sure? How would the JavaScript functions execute simultaneously on a single thread? Async is about interleaving computations on a single thread.
- ilikehurdles 7y agoAsync doesn't have to be single-threaded generally. JavaScript is limited to a single-threaded model, but in other languages (like clojure), futures are tasks run on some other thread. I don't use rust and this article doesn't make any mention of threads so I'm not really sure what's going on here.
- Rusky 7y ago> futures are tasks run on some other thread. This is usually false, in Rust and elsewhere. Futures are tasks that run on whatever thread is scheduling them. They are an even lighter-weight version of green threads/fibers/M:N.
- pimeys 7y agoBut only if you await them in that thread. If you `spawn` them, they can be executed in another free thread; although you'll miss the return value in this case. So for people new to async/await in rust, how a web server usually works: Request/response future is spawned to the runtime, and a thread decides to take the future for execution if the runtime uses multithreading. Inside of this request/response future whatever futures are .awaited will continue the execution in the same thread when ready. Users can also spawn futures inside the request/response future, which might go to another thread. The result of this future is not available for the spawning thread. Some executors have special functions, such as `blocking` that will execute the block in another thread, giving the result back to the awaiting thread when finished.
- ralusek 7y agoThey're asynchronous tasks, there would be no point in using promises with synchronous tasks. So we're not talking about simultaneous execution of functions that are making use of the single thread, we're talking about serial execution of functions on a single thread which are basically doing nothing more than scheduling an asynchronous task and not bothering to wait for the result. Think of it this way. I have 3 letters I need to send, and I'm expecting replies for each. A single threaded, synchronous language, would basically send the first letter, wait for the reply, send the second letter, wait for the reply, then send the third and wait for the reply. In JS, you're still single threaded, but you just recognize that there is no point in sitting around and waiting before moving onto the next item. So you send the first letter, and when it would be time to wait, you continue executing code, so you immediately send the next letter, and then finally send the 3rd. How they're scheduled simultaneously on a single thread is exactly what makes JS so fast for IO. Once it starts making an http call, db call, disk read, etc, it will release the thread to begin execution of the next item in the event loop (which is the structure JS uses under the hood to schedule tasks). So what really happens is when we call asyncA(); asyncB(); JS will go into `asyncA`, run the code, and at some point it will get to a line that does something like "write this value to the database." This will be an asynchronous behavior, that it knows will be handled with a callback or a Promise, so it will immediately continue execution of the code. So now it pops out of executing `asyncA` and executes `asyncB`, meanwhile the call to the DB has gone out and we don't care if it has finished, we'll await both of these when we need them.
- Rusky 7y agoJavascript has two differences that contribute here: * First, it runs the callee synchronously until the first await, which can fire off network requests, etc. * Second, continuations are pushed onto queues immediately- the microtask queue that runs when the current event handler returns, for example. Rust does neither of these things: * Calling an async function constructs its stack frame without running any of its body. * Continuations are not managed individually; instead an entire stack of async function frames (aka Futures) is scheduled as a unit (aka a "task"). So if you just write async functions and await them, things behave much more like thread APIs- futures start running when you pass a top-level future to a "spawn" API, and futures run concurrently when you combine them with "join"/"select"/etc APIs.
- roblabla 7y ago> * Calling an async function constructs its stack frame without running any of its body. This is actually possible to do by using async blocks instead of async functions. E.G. you can write this: fn test() -> impl Future<Output = ()> { // Run some things directly here println!("I am running in the current frame"); async { // Run some things in the await println!("I am running in the .await"); } }
- steveklabnik 7y ago> Awaiting is a completely different concept than invoking, why overload it? The wording is a bit imprecise; you can't 'await' something to invoke it, exactly. It also won't begin execution until .await is called. What happens is, at some point, you have a future that represents the whole computation, and you pass it to an executor. That's when execution starts. There's a join macro/function that works the same way as Promise.all, and is what you'd use for parallelism.
- roblabla 7y agoIn Rust, you can use a future adapter that does this: futures::join!(asyncTaskA(), asyncTaskB()).await See the join macro of futures[0]. The way it works is, it will create a future that, when polled, will call the underlying poll function of all three futures, saving the eventual result into a tuple. This will allow making progress on all three futures at the same time. [0] https://docs.rs/futures/0.3.0/futures/macro.join.html https://docs.rs/futures/0.3.0/futures/macro.join.html
- Buttons840 7y ago"At the same time"? How does that happen on a single thread?
- steveklabnik 7y agoExecutors are not inherently single threaded. They can be, they can also not be.
- ilikehurdles 7y agoIt just interleaves execution. This really seems more similar to python's generator/yield semantics.
- estebank 7y agoThat's how it is implemented under the covers, it uses the nightly only generators feature which builds a state machine of the entire future.
- ralusek 7y agoI don't like this at all. Having to rely on futures::join! means that I don't have the flexibility to control the execution of these things unless Rust adds that specific utility, right? In JS, for example, the `bluebird` library is a third party utility for managing execution of functions. You can do things like const results = await Promise.map(users, user => saveUserToDBAsync(user), { concurrency: 5}); And I pass in thousands of users, and can specify `concurrency: 5` to know that it will be execute no more than 5 simultaneously. Implementation of this behavior in user space is trivial in JS, is it possible in Rust?
- rkangel 7y agoYou would use the join method combine the futures (https://docs.rs/futures/0.2.1/futures/trait.FutureExt.html#method.join https://docs.rs/futures/0.2.1/futures/trait.FutureExt.html#m...). Both will execute at the same time and you'll get a tuple of results.
- krzkaczor 7y agoFunny fact: code in a second snippet (awaiting on promises invoked before) can result in node throwing `uncaught promise rejections` errors. More info: https://dev.to/gajus/handling-unhandled-promise-rejections-in-async-functions-5b2b https://dev.to/gajus/handling-unhandled-promise-rejections-i...
- chetanbhasin 7y agoThis is just the lazy way of handling tasks. Rust allows you to join two futures, and awaiting on the resulting future will run the underlying two in parallel and wait for them. In a language like Haskell where everything in lazy, it's not uncommon for someone to model their logic in such a way that computations that are not necessary are never run but appear to be used in code anyway. Depending on what you want to do, it's also possible to start threads/green-threads (using something like Tokio), and use message passing for async tasks where you do not need to process the result synchronously.
- willtim 7y agoJavaScript automatically starts the tasks and this is bad design IMHO. One loses referential transparency and the ability to run the workflow with different schedulers. It looks like Rust has done a better job.
- ralusek 7y agoJavaScript does not automatically start the tasks. It executes the function if you...execute the function. If you just want to pass around something with deferred execution, you can just pass the function around, or wrap it in a closure.
- willtim 7y agoIf I understand correctly, applying a JavaScript Async function is effectful and starts the background tasks. A better design is for the async function to yield an Async computation when applied. These computations can then be composed and passed around with full referential transparency. Only when we run the final composed Async computation, should the effects happen (threads running our computation). This is how the original F# asynchronous workflows were designed, which predate the C# and JavaScript implementations. Thankfully Rust works this way too!
- adgasf 7y agoIt does automatically start the tasks. JavaScript asyncs are "hot". You can simulate "cold" asyncs using a function, as you describe, but in other languages this is how they work by default.
- nwienert 7y agoI’m not sure exactly what hot is here, but if that’s the case then all javascript functions are hot. It’s behaving consistently with any other function. I assume the point here is that Rust in this case changes the way function calls work based on async, which is, well, inconsistent.
- 7y ago
- jeffdavis 7y agoIn rust, they separate the concept of a future from how it's actually run. Tokio is one way to run a bunch of futures, and it has many different options. For instance, I use the CurrentThread runtime in tokio, because I'm using the rust code as a plugin to Postgres, and it accesses non-thread-safe APIs. What you are asking for is essentially for the futures runtime to be hidden from you. That's fine for some languages that already have a big runtime and don't need the flexibility to do things differently, but doesn't work for rust.
- oconnor663 7y ago> Isn't it kind of a poor design choice that Rust will not actually begin execution of the function until `.await` is called? Begin execution where? If every future started executing immediately on a global event loop, that event loop would need to allocate space for every future on the heap. A heap allocation for every future is exactly the sort of overhead that Rust is trying so carefully to avoid. With Rust futures, you can have a large call tree of async functions calling other async functions. Each one of those returns a future, and those futures get cobbled together by the compiler into a single giant future of a statically known size. Once that state machine object is assembled, you can make a single heap allocation to put it on your event loop. Or if you're going to block the current thread waiting on it, depending on what runtime library you're using, you might even get away with zero heap allocations. This sort of thing is also why Rust's futures are poll-based, rather than using callbacks. Callbacks would force everything to be heap allocated, and would work poorly with lifetimes and ownership in general.
- Matthias247 7y agoThis! Even if some of us would prefer „hot“ futures that immediately start execution from a conceptual point of view they are simply not possible in Rusts async/await model. Executing a Future requires it to be in its final memory position and be pinned there forever. Otherwise borrowing across await points would not work. But if every future would immediately start in its final place you would not be able to return them, and composition would not be possible. That is why we have a Create - Pin - Execute and Await workflow. These issues are not an issue in JavaScript and other languages since objects are individually allocated on the heap there anyway
- leshow 7y agoNote that your first promise doesn't actually do anything in parallel. It just schedules all of those tasks at once. Execution will still be concurrent.
- twodave 7y agoThose don't actually execute simultaneously in Javascript, though. In Javascript, C# and I suspect Java, each usage of await generates a state machine that can _defer execution_. So while one function is blocked, another can continue to execute. The concept is the same in Rust, except in Rust a nested await does not generate a new state machine. The entire async operation just uses a single state machine. The reason Rust does this is because its priorities are different than these other languages. Rust is built around zero-cost abstractions because it is intended to be as fast as possible while still safe. That's one of many reasons why Rust is considered a systems language and Javascript is not. They're for different things. For more on how async in Rust works, I'd invite you to read the actual manual on the subject (linked from the announcement): https://rust-lang.github.io/async-book/06_multiple_futures/01_chapter.html https://rust-lang.github.io/async-book/06_multiple_futures/0...
- Too 7y agoPython has the hybrid approach. Just calling an async function only returns a future but it's execution hasn't been started yet. As with Rust it will only start when you await it. However there is also a way to execute it in the background before the await by calling create_task(future). I found that most code that actually use create_task tend to be super complex and often bug-ridden since eventually you will have to await it anyway, and it is easy to miss this, which will leave errors unhandled, especially propagating cancel() to all these executing futures floating in air.