Is there any benefits to using async-await for a CLI tool?

As part of learning Rust I am building a simple cli tool. Part of the functionality will be making http requests.

I was wondering, is there any benefit in using async-await and having a runtime like tokio?

My impressions is, since a cli is executed, does it thing and exist, there might not be too much benefit of using async with something like tokio. My feeling is that async await is more beneficial when you have a long running process.

Can anyone more knowledgeable about these things share their thoughts?

Thanks

Generally, async code is useful for I/O-bound workloads. That extends to CLI programs: if you're writing something that might process a lot of data each invocation, then it can be worthwhile to use async code for that.

Otherwise, the benefits gained aren't likely to stack up against the added complexity relative to regular rust code.

If you're in doubt, then consider writing minimal versions of both sync and async variants, and benchmarking them on representative inputs using e.g. Criterion.

2 Likes

On the contrary if you have very long pure computations, async won't change anything.

2 Likes

there are CLIs with "background" jobs, notifications coming irrespective of user command entry etc. such cases might utilize async easily imho :slight_smile: just depends on what use-cases you might need to cover

1 Like

The short version is that if you have a "batch" program (user says "do this", program does that and exits), async/await likely isn't going to be much of a benefit, because the program does "one thing" and is finished.

If the program is long running, then async/await becomes significantly more attractive, because the program will be doing "multiple things" concurrently as work requests come in.

This isn't a black and white rule (batch programs can end up doing and benefiting from async IO allowing waiting in parallel; services can be written quite well just using standard threads and work queues), but as a first level vibe check, it's fairly accurate most of the time.

The thing to keep in mind is that async is designed for waiting in parallel. While it can be used for other things, the primary benefit it offers is that multiple async tasks can be waiting on progress to be made elsewhere (most typically IO) without consuming resources. If your program workload doesn't involve waiting, the benefit of async/await is going to be limited.

9 Likes

right, what i implied with my comment was REPL CLIs, long running loopy programs that @CAD97 described in more detail, sorry for potential confusion...

It depends on how you want your tool to behave. For example, I have one that which basically makes a few http requests and reports back to the user. Given the user has to wait for the requests to complete, and the tool doesn't do a lot of work with either the incoming or reported data, there's no need to think outside of a single thread of execution. I do have a separate thread to display progress, but that's just gravy.

However, I'm currently adding a facility for it to do some caching work in the background while the user is waiting (to increase future response speeds), so I'll need some kind of concurrency (whether this be threading or async/await).

Personally rather than start the thinking process with tech ("should I use async/await or not"), I would start with what you want your software to do. In the absence of a requirement-driven need to do otherwise, always choose the simplest (ie. here synchronous, single thread).

2 Likes

The counter question is why not make it async-await? From the developer's perspective the execution path seems blocking; the code reads nearly the same either way. If you do reach the point that async-await becomes a needed feature then it's far better to have that in place then retrofitting.

1 Like

Regarding the use of Tokio, from https://tokio.rs/tokio/tutorial

The place where Tokio gives you an advantage is when you need to do many things at the same time. If you need to use a library intended for asynchronous Rust such as reqwest, but you don't need to do a lot of things at once, you should prefer the blocking version of that library, as it will make your project simpler.

2 Likes

Here's an explanation from the documentation of ureq, the #1 Rust HTTP client according to Lib.rs:

5 Likes

100% thumbs up to this line of reasoning. If the number of concurrent tasks is in the single- or double-digit range, you probably don't need async/await. That ecosystem comes with non-negligible overhead that is unnecessary and even counterproductive in many ways.

In a pseudo-related rant, I'm always confounded by attempts to make GUI designs async by default. An application's interaction with its GUI is almost never I/O bound, and the total number of concurrent tasks is almost certainly going to be on the small end. This is a kind of "everything looks like a nail" approach to development.

3 Likes

GUI isn't typically IO bound, but it is typically heavily timer bound. Promise-style asynchrony without async/await is structured with a lot of continuation passing, and UI animation also typically involves a lot of continuation passing which can be simplified with async/await.

If your UI is fairly static — very roughly, you could describe it just with HTML/CSS but without CSS animations, using page transitions for UI state transitions, even if doing so would be uselessly tedious and a bad idea — then it very clearly won't benefit from async/await.

But as soon as you start having animations involved, async/await starts to have benefits. Each animation is an async task which makes a little progress each frame. If every animation is just updating every frame, async/await still doesn't confer much benefit to offset the waker infrastructure, as there's no dynamic waiting going on, but as soon as you start sequencing animation (with other animations or with domain logic) you start to see real benefits to using async/await to structure things. On a similar bent, it's often an easier structure to use channels and a task waiting on the receiving end than callbacks in Rust, and UI is historically very callback heavy, which can potentially be clarified by utilizing channels instead.

All of that said, though, UI has different needs from an async runtime/reactor than an IO focused one. At a minimum, most IO focused runtimes are going to expect tasks to spend more update cycles waiting than making progress, whereas a UI runtime would probably want special optimizations for polling off of "next update" timers. You probably also want some shared consistent time on each update tick rather than each task using a slightly different system time.

An async UI framework for me firmly falls into the "other specialized uses" where async/await can be useful. It's not simple, and there's more involved in making it worth it than with IO-bound workflows, but it absolutely can benefit after paying that initial startup infrastructure investment.

2 Likes

We do a lot of timer-based things in games, most notably animations. But async/await is almost never present for these.

It's a different model of operation to expect your animations to run while your code semantically blocks as a user of some animator, versus just doing the actual animation every frame because that's what you're doing anyway. Maybe it's the abstraction boundary where you are starting to think about blocking and which style of blocking makes sense.

But that's kind of my point; you can choose a layer of abstraction that doesn't need to block and still have animations.

2 Likes

Doesn't have to be that way though: the async way could be made the default in std, with a default light runtime, and implemented on top of native async OS interfaces when possible (like IO uring on Linux), and the sync API implemented on top of it for legacy, then sync code would be slightly bigger. Async need not be bloated just because it is asynchronous.

2 Likes