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

I'm learning rust and tokio vs async-std confuses me: if I have a library that uses tokio (e.g. tonic grpc), can I use a library that uses async-std and mix calls to functions of those two libraries in the same function?


No, through in some cases yes.

There is also a `tokio02` feature which let's you run tokio inside of async-std.

The problems are:

- The global `spawn` method need to know on which executor to spwan thinks, the ones for tokio look for a tokio executor the ones for `async-std` for a async std one.

- The async-io implementations might require to be run in the executor they are shipped with.

- Some combinators shipped with some runtimes also need to be run in that runtime.

This is a bit annoying. I believe everyone involved would have preferred a single general purpose runtime, but to all kinds of reasons this didn't work out.

The original plan was to provide generic interfaces in the futures library so that most code (including things like grpc) can be independent of the runtime.

But people couldn't agree on a common interface. The main problem was around how to do the `AsyncIo` trait and timers.

Since the introduction of async/await the runtimes have converged a lot more in their external interfaces, and there are plan to put `AsyncRead`/`AsyncWrite` traits into std.

After this the divergence problem might maybe be fixed slowly.

---

To provide some more information about the problem:

Basically a executor consists of three parts:

1. The thing driving/running/polling futures.

2. A reactor handling the async io details.

3. Something to handle things lie `Delay`/`Timeout`.

For the first point the problem is a missing standardized way to `spawn` new independent futures and another way to do so locally (`LocalExecutor`/`LocalSet`).

For the second point it's not so likely that things will converge but we could agree on a way to have multiple `Reactors` running in parallel or similar in ways which should "just work" without to much overhead.

For the third point people currently can't agree one specific implementation as far as I remember once they do we might at least have a generic timer interface.

Anyway by now most of the (new) external APIs of both async-std and tokio now mirror std (tokio copied that once it turned out it was a good idea).

This also means that most implementations can be made to work with any executor by having a feature for any of the executors and then depending on the feature import different things, for most parts most of the code will just work by importing think from async-std instead of think from tokio.


No.

There’s no clean async abstraction later; once you pick a side, you’re stuck with crates that support it.

Some popular crates let you pick using flags which one you want when you add them... but, tldr: no.

It’s not ideal, but it’s very flexible.

Given they weren’t sure exactly what the async runtime should look like, and the strong backwards compatibility promise, they opted for “get something people can use for now”.

...of course, the python packaging story shows this is a stupid idea in the long run, and I believe the plan is to consolidate into a single “std” async runtime eventually.

...but for now, in your situation, you’re basically screwed I’m afraid. Fork the crate and migrate it yourself is probably the only real solution.

The downside is that doing that usually unearths dependencies that are also using the wrong runtime, etc.

For now: Pick one, stick with it.


That's not fully correct. A lot of futures will work independent of the runtime. E.g. all the sync primitives/channels/etc from all sources (futures-rs/tokio/async-std/futures-intrusive/etc) will work with all runtimes.

Then a lot of objects which require a runtime (e.g. an async socket) might still work on a foreign runtime as long as the original runtime is running. E.g. I think you can use async-std sockets from inside tokio, as long as the async-std runtime is also running in the background. Same is true for the other direction to a certain extent, but it's a bit more complicated to set up.

So overall you will need to understand how the runtimes work and if particular pieces can be mixed and matched. If you don't want to invest in that knowledge => stay on one side.

And before someone starts to blame the Rust ecosystem for that: It's not really different in any other environment. You can't easily mix and match GTK, QT, libuv and boost asio eventloops. It's event a lot harder than trying to combine Rusts async types.


> So overall you will need to understand how the runtimes work and if particular pieces can be mixed and matched...

Hm... really?

I was under the impression that it was more correct to say that mixing runtimes is undefined behaviour?

It may work, but just like mutable aliasing using unsafe may not cause memory corruption; it just happens to work in the current implementation, on the current platform, on the current version of rust.

There is no guarantee it will continue to work in the future.

Maybe I misunderstood, but I thought it was a lot more complicated than just starting multiple events loops and you’re fine.


Yes - really! It would be undefined behavior if the runtime doesn't make any promises about it. But some do make the promises that it will work. I think both async-std and tokio make sure IO related futures work from tasks that are driven by a different runtime - otherwise stuff like `future::executor::block_on(task)` would also not work, and neither would the blocking wrappers around some of the HTTP clients.

However the IO objects might need to be initialized within a certain context (e.g. some TLS variables need to be set for tokio). And there might be a few different things one need watch out for. E.g. if you stop the background runtime things might get weird - however this e.g. can't happen with async-std since the background runtime will never shut down.


I couldn’t find any docs on this, got a link by any chance?

Specifically where tokio commits to working with other runtimes.



This seems unrelated.

Some arbitrary 3rd party wrapper does not make any guarantees about the runtimes it wraps.

This is just more “happens to work at the moment”.


You can mix and match C++20 co-routines, C#/F#/VB co-routines, Kotlin co-routines, that is the whole idea.


You also can mix and match rust async/await.

The problem is not the executor of the futures itself.

But the "Reactor" which is needed to do async-io and the "TimerScheduler" which handle thinks like `.delay(..)` or non os-native timeouts.

What you often can do is having a `Reactor` and time sheduler running for all runtimes involved.

This e.g. works very well with async-std due to it's simple design.

But for tokio it's more complex as you need to provide the reactor and some other hints through something comparable to thread local variables (instead of globals).

This is where the `tokio02` compatibility feature comes in which makes sure to provide access to tokios reactor in the async-std future executors.

Another problem is the global `spawn` method. You normally don't want to run two future executors but as long as their is no abstraction layout over spawn you will have to. Furthermore tokio does a lot of fancy things which you likely will never see in a generic API which is another problem.

For example normally if you want to do blocking code you spawn this in a thread pool to not hinder other async code. But in tokio there is a way to "overlap" the pool of blocking and non-blocking code, basically up to n worker threads of the non-blocking thread pool can be marked at blocking at a time. This is one of many thinks which increase complexity which other runtimes like async std avoided due to it not being worth the additional complexity in >99% of the cases.

Except that tokio is to some degree written for that 1% of cases where all that additional complexity is needed. Because a lot of dev time come from people which work for a company which does need it for their product.


I can probably. I guess a few prinicpal/staff engineers can too. But doing cross FFI async function invocations and trying to unify runtimes in that fashion is not a thing that people usually do. And it still wouldn't allow you to call QT socket APIs and Netty socket APIs from the same thread.


You missed the point, the languages I mentioned have it as relevant enough to have the async runtime as part of the standard library, instead of leaving it to 3rd parties.


C++ coroutines are not more „part of the standard library“ than rusts async support. One might say it might be less, since they arrived so late in c++ lifetime and more Production libraries have been built without them und mind.

And again, they don’t make GTK and Qt and boost Asia eventloops interoperable. Even if you implement co_await support for each of them individually


Yes it's VERY important to differenciate between async/await support (co-rutine) and doing async-io.

The problems all come from async-io.

"Pure" async/await can be mixed and matched in rust without any problems.

But the moment you touch IO (including timeout) a reactor is needed and things get complicated.


They surely are defined by ISO C++ standard.

Like any ISO standard, certain details are expected to be implementation dependent in any ISO C++20 compliant compiler, not missing like on Rust's case.


I think smoke is trying to be that abstraction layer? I know it makes it easy to swap out the runtime.


The issue with abstraction layers is typically that they can only offer the minimal amount of common functionality - which is often not good enough.

They also have the tendancy to get outdated and non-maintained, since most users will just go for a runtime directly.


> - which is often not good enough.

Except that it mostly should be good enough.

async-std tried to mirror rust's libstd, and tokio adapted it.

Similar the global `spawn` and the value returned from it work kinda the same +- some naming differences.

Even the `LocalExecutor`/`LocalSet` can be abstracted over to erase their differences.

That also true for all simple usages of timout.

The remaining differences are:

- different internal implementation details

- naming differences which are not that easy to abstract away

- some detail about time/timeout handling I forgot which for many use-cases doesn't matter

- the AsyncRead/AsyncWrite traits but this is going to get into std so this will go away soon

- some "advanced" features like the fact that tokio overlaps the non-blocking executor thread pool with a blocking worker thread pool, but that in many case not a very important feature.

- some differences in e.g. how to specify the number executor threads etc.

Most other differences are "old legacy left overs" from the pre-async/await futures time periode.

So while a generic in-language abstraction likely won't happen anytime soone one which uses feature flags and re-exports as basis should be very doable in the close future.

(I don't know how good the mentioned one is.)


Same thing happened in Ocaml land w.r.t. async vs lwt. Not sure what the current situation is but people had to abstract their libraries and do 2 implementations to support them.




Consider applying for YC's Winter 2026 batch! Applications are open till Nov 10

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

Search: