New to Async-Await in Rust and first thing I am noticing is that there seems to be an overlap of functionality between Futures crate and a runtime like Tokio.
For example there is join in Tokio which looks similar to what join_all does from the future crates. Both have select which sort of does the same thing.
My question is, what is the heuristics for determining which to reach out for? Is there any general guideline?
If you are writing library code, it's best to make it not care about which executor/runtime you are using, and not depend on the large Tokio library either. So, in that case, use futures (excluding futures::executor), or perhaps even the individual crates that futures re-exports, like futures-channel.
If you are writing an application which runs on Tokio, or a library which requires Tokio IO operations, then you might as well use everything that Tokio provides.
Excuse my ignorance but how does that work out? What if I want use your library that uses low level futures and the like in my program that uses tokio? Does that just work or what?
It does just work. The things in the futures crate do not depend on any details of the runtime - they simply depend on how std::future::Future is defined to work. As a result, they'll work with any runtime, but won't take advantage of any optimizations the runtime can provide.
In contrast, the things in tokio can assume that they're running on the Tokio runtime, and can be optimized on that assumption. They can thus do better than the generic stuff, at the expense of not running properly on non-Tokio runtimes.
Note that the things in tokio::sync are executor independent. (I wish it was a separate crate to make that clear, and allow using it more widely, but it isn't.)
Interesting. Anyone got a short and simple example?
Currently I think of using futures, 'pin' and all that like dropping into assembler in the model of a C program. I really don't want to do it unless I have to. Is that a reasonable attitude?
Currently, the join and select macros are also executor-independent. The question is more around the guarantees the Tokio team want to offer in the long run - do they want the freedom to reach in and depend on details like the co-op budget, or will they never use that freedom?
That is a reasonable attitude - if your runtime crate provides functionality (e.g. tokio::join), then that functionality should be as good as the functionality in the futures crate.
If you're not depending on a specific runtime, however, the futures crate is also high-level, but provides runtime-independent options. And if your runtime doesn't provide functionality you need, but futures does, then using futures is a perfectly good approach.
Hmm.... Apart from a couple of application I have using tokio mixing up run-times like this seems like mixing up React and Angular GUI frameworks in a web application. One just would not want to do that.
Note that futures is not a runtime. tokio is, as is async-std, as is smol. Instead, futures is a set of runtime-independent utilities.
So, the rules of thumb become:
If you do not depend on a specific runtime, use futures (because you don't want to tie your users to a specific runtime).
If you do depend on a specific runtime, use functionality from the runtime in preference to futures. It should never be worse than the generic functionality in the futures crate, and may be better (e.g. Tokio's JoinSet<T> is more efficient than using a generic FuturesUnordered<JoinHandle<T>>, even though they do the same thing semantically).
If your runtime does not provide functionality you need, look for it in futures before you write it yourself, and reference the version in futures if you file an issue with your runtime.
Hmm...My impression was that futures, poll, pin... were the low level, primitives, that one needs to build an async run-time. Such that the application programmer did not need to worry about the details of the underlying mechanics. Am I confused?
You are correct that you do not have to worry about poll in application code, but the futures crate provides things that aren't that low level. For example, there is the StreamExt trait. It also provides channels similar to the ones in Tokio.
I think Tokio's executor (tokio::runtime) is only included when you use the rt feature. So I guess depending on Tokio isn't such a big dependency (in regard to compiled code) if you do not use features = ["full"].
I'm not sure if splitting up Tokio into many small crates would make things easier or more confusing. I guess you could have a crate that re-exports everything for convenience, but I'm usually more confused by this (e.g. with the num crate). But maybe the practice to split up crates has some advantages.
P.S.: Also note that crates.io doesn't allow a hierarchy in its namespace. This may be another reason why someone might want to create a single big crate rather than many small ones. By having a single big crate, you have only one identifier (e.g. "tokio") – with the feature flags being clearly subordinated/subsidiary.
The futures crate is mostly high-level utilities for working with futures (see the things re-exported by futures::prelude, which includes things like TryFutureExt, shared futures and other useful things when you're implementing things that use futures.
It also has some low level bits that you can safely ignore.
I guess my problem is that I have no idea what one might implement with futures if it were not an async run-time. And we already have those. What else would I use futures for?
The futures crate provides utilities that are helpful when implementing things like network services - select and join combinators, for example, plus and_then, map and other ways to build a large async fn from smaller pieces.
It has channels and async/await friendly locking primitives you can use with any runtime, and it provides "Streams", which are async equivalents of iterators, plus "Sinks", which are asynchronous consumers of values.
Not all of these utilities will be useful in all environments - if your runtime provides equivalents, for example, you should use those - but they are useful if you're writing a library that doesn't want to be tied to a given runtime (so you can't use tokio::join, for example), or if your runtime doesn't provide an equivalent (e.g. Tokio has no equivalent to Streams or Sinks at the moment).