Asynchronous, Concurrent, and Futures Development Best Practices

I am hoping I could get some best practices pointers on the development of my first Rust application. There are several concerns that I am running into that make me hesitant to continue the development of my program without first addressing.

System Assumptions: No Data Center; High Latency; Low Bandwidth; Commodity Computing; Dynamic Peer Population of 1-N

Core Concerns:
1.) I am currently utilizing futures, std, and async-std for my async/await functions and executor. There are many functions that seem to be part of each of these libraries. Upon researching the topic I am typically confronted with tokio, and differing implementations. What is the best practice for when to use std functions vs their asynchronous counterparts?

2.) For the executor, should I go ahead and make the switch to tokio (seems like this may be a fairly large overhaul)? Or will async-std, or even smol due to the commodity computing assumption, serve me best/good-enough for now? (keeping in mind potentially years of future development and possible integration as a core mission critical system vs. the need to be at least demo ready for continued funding by end of year)

3.) At what point am I awaiting too many futures? Do I need to create logic breakpoints where I spawn a thread? Or is intelligent placement of block_on(), .await, a.join(b), and select! statements sufficient? I am concerned nodes will freeze on an inter-dependent future, or that with the smallest potential commodity computing node, the program will simply be unable to poll so many futures efficiently.

4.) Are Rust applications really such large files. My current workspace is almost 5GB and nowhere near done. Is this typical? Or is it more likely that I am importing more features and libraries then necessary?

P.S.) Here is a link to another topic I opened with some example code showcasing my current approach to a specific functionality of my program if you'd like a reference:

Usually, when both std and futures/Tokio/async-std provide something, it's because you should always prefer the asynchronous version. The main exception I know of is Mutex and RwLock where the std version is still preferable for some use-cases.

Typically, the thing that makes the async version preferable is that it doesn't block the thread. You can read about this concept in this article.

(As a similar point, I generally suggest that people use the Tokio version rather than the futures version when both provide the same thing. I'm not too familiar with the implementations in async-std.)

I work on Tokio, so my opinion here is somewhat biased, but I believe that Tokio is the best choice of runtime for several reasons, e.g. much more active maintainers, a much larger set of libraries, and a more complete runtime implementation.

As for how much work the switch would be, this strongly depends on whether you depend on specific libraries you cannot easily replace. Tokio has direct equivalents to the majority of what async-std provides. Do you have any such libraries, or any other missing pieces?

Keep in mind that the futures crate is perfectly fine to use in Tokio.

Generally, your limit here will be memory, so look at how much memory your program is using.

The reason behind this is that a future doesn't consume any CPU resources at all when it is idle. Your CPU will run out of juice if you do too much work in your tasks, whether that is in a single Tokio task, or across several tasks. Completely idle tasks will not impact this metric.

I notice that your linked reference uses block_on within an async function. This is not intelligent placement of block_on β€” just use an await instead. In general, you should pretty much never use futures::executor::block_on for anything.

If you had used Tokio both for the main macro and the block_on call, then Tokio would have caught this issue and told you about it by panicking.

The specific example you posted isn't actually a problem since it's in the main function which is usually special in ways that neutralize the issue, but the point still stands.

In my experience, a large part of the target folder is stuff for incremental compilation, or old artifacts it isn't using anymore. I would bet that it isn't quite as large if you delete the target folder and try again.

My usual recommendation about this is "don't worry about it". Worry about the size of your final executable file, sure, but not the workspace.

3 Likes

Thank you for the helpful information!
I am going to go ahead and make the conversion to tokio, and any potential asynchronous std functions, as soon as I finish figuring out the issue with my stream, currently being discussed in the other thread.

Will there be any system dependencies for my .exe that I need to be aware of? Anything that I need to package with the .exe for quick deployment onto ambiguously configured nodes (assume end-user knows nothing of computers)? (i.e. differing OS {limited to Linux, possibly Raspbian, and Windows. 32 & 64 bit }, Processors, Memory, and Storage).
This is my first application and so I am worried come demo time that there will be a lot of missed details that new developers make, which will cause undue difficulty during future dev iterations.

Rust and Tokio generally don't come with any system dependencies (other than glibc). There may be other crates in your project that do come with such requirements. You can find out as explained here.

2 Likes

In order to avoid accidentally mixing futures::executor with another async executor, I suggest removing it from your dependencies. You can do either of these:

  1. Adjust your futures dep to not include the "executor" feature:

    futures = { version = "0.3.25", default-features = false, features = ["std", "async-await"] }
    
  2. Use the individual crates (futures-core, futures-util, etc.) rather than futures which reexports the whole set; this is noisier, but avoids the hazard of unintentionally depending on a feature some other crate in the dependency graph enabled.

In either case, you should then observe futures-executor disappearing from your Cargo.lock file.

4 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.