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::executor), or perhaps even the individual crates that
futures re-exports, like
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?
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
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.
In general, if you're using Tokio at all, I recommend using the Tokio version whenever both futures and Tokio provide it.
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.
futures is not a runtime.
tokio is, as is
async-std, as is
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.
tokio::sync module hooks in to the Tokio runtime's coop system, which means that it cannot "just" be moved into a separate crate.
(The coop system hooks are a no-op outside of the Tokio runtime, which is why it also works outside of it.)
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?
Well, for example, a web server could be implemented using async/await.
futures crate provides utilities that are helpful when implementing things like network services -
join combinators, for example, plus
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).
This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.