Escaping from sync to async world with tokio, and vice-versa
Motivation
Writing rust is a great pleasure when the code is either purely synchronous or asynchronous. However, the awkward thing kicks in if you try to jump between the two worlds. Although there is a standalone document page explaining how to bridge the code, we could always find people having a hard time doing it everywhere, including tokio issues, rust user forum and stackoverflow. This blog post tries to serve as a cheatsheet to glue the code between the two worlds.
Async -> Sync
Usually, this direction is not an issue.
Find tokio
wrappers
Except for runtime
implementation, tokio
also has a large number of wrappers of common sync
operations like reading a file or network socket. If there is one, it is usually the best choice.
Use tokio::task::spawn_blocking
If the blocking operation doesn’t have a wrapper, we can simply do it with tokio::task::spawn_blocking
as suggested from the official docs.
Use tokio::task::block_in_place
spawn_blocking
sometimes is not ideal because it requires the future to be 'static
, i.e. you can’t have any borrows. An alternative would be tokio::task::block_in_place
, which internally spawns another thread to replace the current thread to avoid blocking the whole runtime.
Caveat: Don’t “Just Block”
In any case, blocking should be avoided in worker threads. In other words, long blocking without yielding to the underlying runtime by any await
can lead to various issues, like deadlocks and missing signals, etc.
Sync -> Async
This direction is much more common (imagine you want to call async
functions in a trait
) but easier to mess up as well. Using the code below as an example, we illustrate different solutions.
1 | async fn to_bridge() { |
If the calling thread is managed by tokio
If the main entry is annotated by tokio::main
or current thread is managed by tokio
, a perfectly safe approach will be wrapping block
in a spawn_blocking
. You can also try this in the playground.
1 | async fn to_bridge() { |
But this approach is not perfect, sometimes things could be complex, for example, the call stack is deep and you can’t ensure the block
function is indeed wrapped with spawn_blocking
. In this case, the program above without spawn_blocking
will panic with (see playground):
Cannot start a runtime from within a runtime. This happens because a function (like
block_on
) attempted to block the current thread while the thread is being used to drive asynchronous tasks.
Thankfully, block_in_place
again comes to the rescue. We can do (see playground):
1 | async fn to_bridge() { |
Note block_in_place
can also block if the block thread reaches the limit.
This trick is also widely used to call async
functions when in Drop
.
If the calling thread is not managed by tokio
Sometimes, the calling code is the pure synchronous code and you even don’t find tokio
in their dependency. In this case, the Handle::current()
in previous approach will panic because there is no runtime. In this case, we can spin up a current_thread
runtime. See it in playground.
1 | use std::time::Duration; |
If necessary, the current_thread
runtime can be created once and saved for later use, though creating one is actually very lightweight.
All-in-one takeaway?
Sure, here is the takeaway:
1 | fn escape_to_async<F, O>(fut: F) -> O |
You can even play it with:
1 | async fn jump_sync(level: usize) { |
Note if you would like escape_to_async
to be compatible with current_thread
runtime, O
and F
must be Send
because tokio
currently doesn’t allow current_thread
to block_in_place
. That is:
1 | fn escape_to_async<F, O>(fut: F) -> O |
It somehow seems not perfect but I failed to figure it out in the scope of tokio. I’m happy to hear about a better solution!
Caveat: Don’t mix with other executors
It can be tempered to use other executors like futures::executor::block_on
to escape from sync to async world. However, it is generally a bad idea, which is known to cause deadlocks.
Conclusion
Generally speaking, because async
inexplicitly requires an executor (or runtime) while sync
code doesn’t, users should be very cautious when transiting from sync
to async
, while the other direction is much more straightforward.