I am trying to figure out a recommended way of calling async method in a sync method, which already would be running in async context. That is, the library is async already, but there is one user facing method which is sync, in which I want to call an async method.
I have a method (I cannot change this to async -- since this is public API):
pub fn sync_method(&self) -> Result<()> {}
Within this method, I want to call an async method (and move next only after the async method is completed), so I did this:
let _ = tokio::task::block_in_place(move || {
let rt = tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap();
rt.block_on(async {
let _ = db.my_async_method().await;
})
});
this worked when I used it in the code / examples. But cargo test started failing:
can call blocking only when running on the multi-threaded runtime
The tests all have #[tokio::test], and seems tokio tests run in single thread. So I guess that's why they failed.
I thought I would use Runtime.block_on:
let rt = tokio::runtime::Runtime::new().unwrap();
rt.block_on(async {
...
});
but this breaks too! cannot create runtime within another one. So I have settled with following:
The blocking ("sync") code must be run in a separate thread, such as with Tokio's spawn_blocking(). No other option is fully composable and thus suitable for a library.
block_in_place() is not appropriate for use in a library, because it will cause misbehavior of select!() and similar things in the caller. Only use block_in_place() when you control all of the async code that is calling it, from the task on down. You must use spawn_blocking() (or some other approach) instead.
Don't create a second runtime; get a Handle to the existing runtime and use that.
This way is incorrect. By using std::thread::Thread::join() you are bypassing Tokio's correctness checks because Tokio can't notice you are calling join(). join() is a blocking function so you must not call it from an async context.
This is the shape of a correct async-blocking-async transition:
let handle = Handle::current();
spawn_blocking(move || {
// ... call something that calls sync_method() which internally calls ...
handle.block_on(async {
let _ = db.my_async_method().await;
})
}).await;
If you can't pass the Handle through, then obtain and call it immediately like Handle::current().block_on( instead; the price of that is that your users will get the “must be called within the context of a runtime” error if they call your sync_method() from a thread without enter()ing first, so it's better to keep the handle if you can.
If you do all of this correctly, your library will run fine under either the current-thread or multi-thread runtimes.
Whenever possible, you should avoid having a blocking async → sync → async sandwich in your program. Its messy and it's better to change it to be all async instead of hacking around it.
Unless there's some hard limit that forces use of a sync API, like C FFI that doesn't support anything else, or your library's public API being committed to being sync, try making the whole call chain async, by changing fn to async fn.
Sometimes if you have functions that must return something synchronously, like callbacks on the Iterator, you can try returning a Futurewithout running/awaiting it, and make it the caller's problem. There's join_all in futures crate that can await any number of futures once they're bubbled up out from the sync calls.
Another option is to use channels that support both sync and async sending/receiving. This allows you to run sync and async code independently, on separate threads. For example you can spawn a thread for a single-threaded blocking database interface, and communicate with that thread via channels from anywhere in an async program.
Thing is, sync_method is public and it is at the top level. So library users would call this method directly. If above is the only right way, then it would be a breaking change. In that case, I could just change the sync_method signature to async.
If you can put the Handle in whatever data type some_method is a method of, then that won't be a breaking change.
If that isn't possible then there is no option that isn't a breaking change (other than setting up a tokio runtime in a static variable, which is not usually something a library should be doing).
Oh, yes, async callers of sync_method must do that.
If your sync_method was previously documented as, or de-facto used as, a non-blocking method (i.e. OK to use from either async or non-async code), then making it block is a breaking change.