> However, if a library uses async, you have little choice but to make your whole project async.
This isn't true. I'm writing an application right now that is mostly sync, but has a small amount of async code in it. It's easy to spin up a tokio runtime which can run inline on the current thread. Then use it to evaluate a Future.
(I thought tokio had a helper like this too but could only find `tokio::runtime::Runtime::new().unwrap().block_on(async { println!("I'm async!"); });`.)
Needing a helper library for something as simple as async so you don't go mental is really not good enough. I see the same thing with error handling - every Rust project I see imports a helper because it's too clunky otherwise.
If you don't want to pull in a helper library to run async code in a sync context, then why pull in an async library at all?
Rust is not a batteries-included language like Python. There are lots of libraries that are very commonly used in most projects (serde, thiserror, and itertools are in almost all of mine), but this is a conscious choice. They say in Python that the stdlib is where projects go to die. I'd rather have the flexiblity of choosing my dependencies, even for stuff I have to use in every project.
The problem is that a large number of popular libraries has converted to async, 95+% of them to Tokio.
So you are stuck with smaller, less battle tested products if you'd rather not pull in 100+ crates of dependencies that are doing nothing but inflating the build times and file sizes (for your particular usecase).
OK, but like, can we just be honest then that the problem here is that your build times go up? People act like it's an insurmountable problem rather than just a trivial trade-off where, yes, your build times will go up because of some extra dependencies on an async runtime.
Increased build times are not great but holy shit the way people talk you'd never know that that's the actual trade-off here, an extra 3 seconds on a clean build.
Usually you're reaching for block_on because a library you want to use is async. Almost certainly the library you're using will have already be depending on an async library, so by pulling it in yourself you're not adding additional dependencies.
This is the kind of thing you write once to abstract out async things in a function and call it a day. It really isn't that bad. Besides you can just use:
On one hand, I agree that on the surface, this looks complex, if you don't read it.
But on the other hand, just read the code. It's not complex.
You drill down into a tokio namespace. You make a builder object. You unwrap it. This is idiomatic Rust. It's verbose, but explicit is better than vague. There's no conditional logic. There's no weird type-fu. No macros. There's not even any parameters to supply, other than the Future to block on.
It's trivial to write a wrapper to go from chained methods a helper call.
Just noting for other readers that, while killercup posted another option using `smol`, this seems to me to be in line with Rust's philosophy of explicitness, which is something I really appreciate about the language:
- create a builder
- run on the current thread only
- enable all drivers
- create the instance
- ignore errors
- then call some blocking async code
Would look nicer if split out onto multiple lines I imagine:
use tokio::runtime::Builder;
let runtime = Builder::new_current_thread()
.enable_all()
.build()?;
runtime.block_on(async { println!("I'm async!"); });
This isn't true. I'm writing an application right now that is mostly sync, but has a small amount of async code in it. It's easy to spin up a tokio runtime which can run inline on the current thread. Then use it to evaluate a Future.