3 ms·
> When designing an API in Rust which performs IO, you have to make a decision whether you want it to be synchronous, asynchronous, or both. Why must that be t
by ioquatix 5y ago
> When designing an API in Rust which performs IO, you have to make a decision whether you want it to be synchronous, asynchronous, or both.
Why must that be true? Why can't you write the interface once, and have concurrency be an implementation detail?
- vlovich123 5y agoBecause async based APIs need to return a promise type of some kind (eg Future). You can kind of auto-generate wrapping code to convert synchronous to asynchronous (with some performance cost that may be undesirable) but you can’t generally do the reverse, unless you try to do funky things like pausing/resuming user space fibers (and then issues of lock inversions and things come into play there in a systems level language).
- monocasa 5y agoBecause rust sync versus async colors the functions and how they're called.
- Kinrany 5y agoIdeally: 1. The compiler would be able to turn a subset of async code into sync code with no runtime cost 2. Awaiting would be the default, with `.await` deprecated and special syntax for getting a raw future instead That way most code would look the same regardless of being executed synchronously or asynchronously, with exceptions for evaluating multiple expressions in parallel and such. But #2 probably implies lazy evaluation semantics for all expressions!
- wahern 5y agoBecause the design of async in Rust precludes that. Some of the details are leaky. For example, recursive functions require dynamic allocation in an async context because the async state must be statically sized. See https://rust-lang.github.io/async-book/07_workarounds/04_recursion.html https://rust-lang.github.io/async-book/07_workarounds/04_rec... There's no easy way to get around the function color problem unless you went the way Go did. But Go's choice made C ABI interoperability more complex. Rust chose simpler C ABI interop, at least for the sync case--no matter which choice you make neither approach makes async interop seamless. The fun part will be seeing how Rust integrates async and fallible allocation. Both of these issues you could see coming from 10 years away, and also see how they'd interact, but Rust devs decided to punt on some of these hard decisions early on. This sort of wheel reinvention is what you typically see in every new language, unfortunately, and you typically see them resolved in much the same way because solutions are path dependent on very early design decisions, and almost everybody makes the same early decisions. Except for Go. Go made the decisions it did because the designers had decades of language design experience, including decades of async experience under their belt. Rust designers came with a different set of experiences and goals, and this shows. (Not saying Go is better than Rust--in fact, non-fallible allocations was always a show-stopper for me in some critical niches. But Go made the most difficult decisions up front, and that included putting async first.)
- steveklabnik 5y agoRust did not punt on concurrency early on. The seventh word used in the first sentence ever describing Rust is "concurrent" http://venge.net/graydon/talks/intro-talk-2.pdf http://venge.net/graydon/talks/intro-talk-2.pdf It even comes before "safe"! What did happen was that the ways in which concurrency was implemented changed as other design constraints on the language changed. But 1.0 wasn't released until we knew what the concurrency story for Rust would be, even if sorting out all of the details took a few years. "leaky" is in the eye of the beholder. Yes, if you think this should be abstracted, then it's a leak. But not everyone thinks that it should; many things about concurrent vs sequential are different, and Rust likes to expose certain kinds of costs and promises in the type signatures of functions. For its core audience, this is not a leak, this is giving you important information about the context the function should be used in.
- wahern 5y ago> Rust did not punt on concurrency early on. Rust started with green threading/fibers then quickly rejected that approach. Then it spent years iteratively building an alternative solution, which is still underway. That's punting in my book; and it's punting for the majority of Rust aficionados who are surprised by the various twists and turns things take as the solution (as inevitable as it is) slowly materializes. By contrast, nothing of substance about Go async has ever changed, except perhaps the change in the default value of GOMAXPROCS. It was complete at conception. It wasn't a wrong decision that Rust made; it was just a choice. But 10 years out it's not entirely implausible that if Rust had stuck with fibers that it may have driven the required OS improvements (e.g. Google's User Managed Concurrency Groups (UMCG) Linux kernel patches) that would have resolved some of the issues. It's not like Rust has become ubiquitous in the embedded space either, considering that it's held back by LLVM in that regard. Something similiar happened with fallible allocations. Very early on most Rust devs declared that they believed that attempting recovery from allocation failure was folly (which in the land of GUIs from whence most of them came was the near universal opinion), and so shot down attempts to consider fallibility in the APIs.[1] Cue 10 years of slowly walking that back, with iterative (and still mostly pending) changes that were less than ideal owing to the fact that handling allocation failures is made infinitely more difficult if you don't take it into consideration at day 1. [1] And, no, it's not enough to say that Rust core is allocation agnostic, because setting aside that only a tiny minority of Rust programmers only stick to core, the decision involved setting idioms and practices surrounding panics.
- dathinab 5y agoIt not possible with rust current abstractions you would need: - higher kind types / type constructors - some features to handle differences in the auto-traits depending on the result of the type constructor - some magic to resolves that --- - OR namespace overloading e.g. read_to_string$sync and read_to_string$async and magic to resolve that But async has A LOT of implications which change subtle things around handling it, like e.g. the handling of lifetimes/borrows, Send, Sync, etc. So this probably wouldn't be worth the complexity it introduces.
- ibraheemdev 5y agoBecause synchronous IO functions block the current thread and return the value directly, while asynchronous function return a `Future`, which will eventually resolve to the value, and can be polled concurrently with other futures as to never block. fn sync_read() -> Vec<u8> { ... } fn async_read() -> impl Future<Output = Vec<u8>> { ... } // the second can be written more succinctly as: async fn async_read() -> Vec<u8> { ... }