Hacker Newsnew | past | comments | ask | show | jobs | submitlogin

In 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



I don't know Rust but I understand the question if await is the only way to yield execution.

Without a yield instruction its strange to ask "how do I start all these futures before I await" and join does make sense because it does both of those things. But other languages can start futures, yield, reenter, start more futures, and wait for them all while making progress in the mean time.

I'm curious what the plan in there.


Join doesn't do everything. It's just a way to take multiple futures, and return a Future wrapping them all. I think there's a misunderstanding of how Futures and Executors interact in rust here which is why everyone is having a hard time understanding things.

A future in rust is really just a trait that implements a `poll` function, whose return type is either "Pending" or "Ready<T>". When you create a future, you're just instantiating a type implementing that function.

For a future to make progress, you need to call its poll function. A totally valid (if very inefficient) strategy is to repeatedly call the Future's poll function in a loop until it returns Ready.

All the `await` keyword does is propagate the Pending case up. It can be expanded trivially to the following:

    match future.poll() {
        Pending => return Pending,
        Ready(val) => val
    }
Now, when we create a top-level Future, it won't start executing. We need to spawn it on an Executor. The Executor is generally provided by a library (tokio, async-std, others...) that gives you an event loop and various functions to perform Async IO. Those functions will generally be your leaf-level/bottommost futures, which are implemented by having the poll function register the interest in a given resource (file descriptor or what not) so that the currently executing top-level Future will be waken up when that resource has progressed by the Executor.

So if you want to start a future, you will either have to spawn it as a top-level future (in which case you cannot get its output or know when it finishes executing unless you use channels) or you join it with other sub-futures so that all your different work will progress simultaneously. Note that you can join multiple time, so you can start two futures, join them, and have one of those futures also start two futures and join them, there's no problem here.


This is essentially what I assumed and what I believe what ralusek assumes to be the case. This does not change our question.

What would be the syntax for how you would spawn a future, add it to the current Executor, cooperatively yield execution in the parent such that progress could be made on the child, but also return execution to the parent if the child yields but does not complete?

In C# I believe you could simply call

    Task.Run(Action);
This will schedule the action to the current executor. If you yield execution through await that task will (have a chance to) run. The crux of the question is that the fact that you do not need to await that specific task, you just need to cooperatively yield execution such that the executor is freed up.

    var shortTask = Task.Run(Task.Delay(100).Wait);
    var longTask = Task.Run(Task.Delay(500).Wait);
    await Task.Delay(1000);
  
    await shortTask;
    await longTask;
In C# this will take ~1000 milliseconds as the child futures yield execution back to the parent such that it can start its own yielding task.


With async-std, you can just do

    let future = async_std::task::spawn(timer_future());
And then do more work. The timer_future will run cooperatively, and the future returned by spawn is merely a “join handle”.

But this is a feature of the executor, not something that’s part of the core rust async/await. With tokio, you’d have to spawn a separate future and use channels to get the return value.


It's the same. :)

    use std::time::Duration;
    use async_std::task;
    
    let shortTask = task::spawn(task::sleep(Duration::from_secs(1)));
    let longTask = task::spawn(task::sleep(Duration::from_secs(2)));

    task::sleep(Duration::from_secs(5)).await;
    shortTask.await;
    longTask.await;
or simply:

    futures.join!(
        task::sleep(Duration::from_secs(1),
        task::sleep(Duration::from_secs(2),
        task::sleep(Duration::from_secs(5)
    ).await;


I 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?


The async map you laid out above could be accomplished with a `Stream` in async Rust. You can turn the array into a `Stream`, have a map operation to return a future, and then use the `buffered` method to run N at once:

https://docs.rs/futures/0.3.0/futures/stream/trait.StreamExt...

Not only does the `futures` crate provide most things you'd ever want, it also has no special treatment – you can implements your own combinators in the same way that `futures` implements them if you need something off of the beaten path.


It feels like you're complaining about things in the Rust language without taking the time to understand how the language idioms work. RTFM.

Additionally, you're making snarky comments about how you don't like how the base language doesn't handle something like JS...then reference a third party JS library. Base JS doesn't solve your 'problem' either.

To answer your question, async/await provides hooks for an executor (tokio being the most common) to run your code. You things like that in the executor.

https://docs.rs/tokio/0.2.0-alpha.6/tokio/executor/index.htm...


I reference a third-party library to show that you have full control of this stuff in user-space. Implementing Promise.all or Promise.map in userspace is trivial.

I'm not complaining about things without taking the time to understand how the language works, I'm giving examples of things that don't seem possible based off of my understanding of how the language works...in hopes that someone will either clarify or accept that this is a shortcoming.


Rust and JS have very very different execution models. You can absolutely control how many futures are allowed to make progress at the same time fully in userspace. If you're joining N futures, and want to only allow M futures to make progress at a time, make an adapter that only calls the poll function of M futures at a time, until those futures call ready.

Rust gives you all the flexibility you need here. It might not be trivial yet because all the adapters might not be written yet, but that's purely a maturity problem.

The `join` macro does nothing magical. Go check out its implementation, and it will make it obvious how to implement a concurrency argument.


Fortunately, `futures::join!` isn't provided by Rust- it's provided by library code. I am not aware (off the top of my head) of an existing equivalent to your `Promise.map` example, but it is also implementable in userspace in Rust.

The primitive operation provided by a Rust `Future` is `poll`. Calling `some_future.poll(waker)` advances it forward if possible, and stashes `waker` somewhere for it to be signaled when `some_future` is ready to run again.

So the implementation of `join` is fairly straightforward: It constructs a new future wrapping its arguments, which when polled itself, re-polls each of them with the same waker it was passed.

There are also more elaborate schemes- e.g. `FuturesUnordered` uses a separate waker for each sub-future, so it can handle larger numbers of them at some coordination cost.


The macro is just a wrapper around this (and a couple of other functions): https://docs.rs/futures/0.3.0/futures/future/fn.join.html

And from a quick scan of the source it doesn't look like anything there is impossible to implement in userspace: https://docs.rs/futures-util/0.3.0/src/futures_util/future/j...


The join macro is also implemented “in user space”.


Thank you, so what is the syntax that is used in order to execute without awaiting? Do you create a thread for each?


This is where things get tricky: Rust didn't standardize an event loop - it only standardized common traits that allow implementing one, and a way to communicate whether a computation needs more time or already has a result.

If what you want to do is run multiple CPU-bound computation and have a central event loop awaiting the result, then yes, you'll need to spawn threads and use some kind of channel to transfer the state and result. If what you want is to run multiple IO-bound queries, then you'll want to use the facilities of the event loop of your choice (tokio, async-std, etc...) to register the intent that you're waiting for more data on a file-descriptor.

The "proper" way to execute without awaiting it on the current future is usually to spawn another future on the event loop. The syntax to do that with tokio is

    use tokio;
    let my_future = some_future();
    tokio::spawn(my_future);


You do not create a thread for each- that would defeat most of the purpose of futures.

Instead you call `Future::poll`, which runs a future until it blocks again, and provide it a way to signal when it is ready.

That signal would be handed off to an event loop (which tracks things executing on other hardware like network or disk controllers) or another part of the program (which will be scheduled eventually).


In Rust, everything is in ‘user space’ since nothing is managed by the language itself, you have full control on how the Futures are executed. Even the executor (the “event loop” in js parlance) is a third party library. You can't get more control in js than what Rust gives you.


"At the same time"? How does that happen on a single thread?


It just interleaves execution. This really seems more similar to python's generator/yield semantics.


That's how it is implemented under the covers, it uses the nightly only generators feature which builds a state machine of the entire future.


Executors are not inherently single threaded. They can be, they can also not be.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: