13 ms·
Really, the problem isn't tokio. The problem is this: > An inconvenient truth about async Rust is that libraries still need to be written against individual ru
by wokwokwok 3y ago
Really, the problem isn't tokio. The problem is this:
> An inconvenient truth about async Rust is that libraries still need to be written against individual runtimes.
That's really the heart of it. If it was really just a runtime, it wouldn't matter what implementation you plugged in.
...but it's not true for the rust runtime; I mean, it's understandable, how can you have one runtime that is multi-threaded and one runtime that is not, and expect to be able to seamlessly interchange them?
I understand it's hard and lot of work went into this, but let's face this. This article is right:
Practically speaking, tokio has become 'the' rust async runtime; but it's an opinionated runtime, that has a life cycle and direction outside of the core rust team.
That wasn't where we intended to end up, and it's not a good place for things to be. I, at least, agree: avoid async. Avoid teaching rust using async. When you need to use it, partition off the async components as best you can. I <3 rust and I use it a lot, but the async story stinks.
We should have an official runtime, officially managed, and guided by the same thoughts that guide the rest of the language.
What we have now is a circus. After 4 years of async being in stable.
- Ygg2 3y ago> We should have an official runtime, officially managed, and guided by the same thoughts that guide the rest of the language. If it goes into official runtime, then backwards compatibility will kill it eventually. You'll have situation where in year 2078 someone will ask why are we still having tokio when everyone is using telepathy lib? > What we have now is a circus. After 4 years of async being in stable. It's caused by strong backwards compatibility guarantees and long RFC process + unexpected problems. Without strong backwards compatibility, no one would be using Rust. RFC exists to hash out unexpected problems but so far we can't peer in the future. Here is an example: Want to make Range from non-Copy to Copy. I.e. make a new type, rename old to new. That will be one year for RFC and two edition to stabilize circa Rust 2028. By that measure async fixes have been blazingly fast.
- thesuperbigfrog 3y ago>> If it goes into official runtime, then backwards compatibility will kill it eventually. You'll have situation where in year 2078 someone will ask why are we still having tokio when everyone is using telepathy lib? This kind of situation happens and leads to a second, newer official runtime getting adopted and the older, legacy runtime being supported as long as is needed. This happened with Java's official GUT toolkits which started off with the Abstract Windowing Toolkit (AWT) and then moved to Swing and almost moved to JavaFX. Having multiple officially supported core components is not necessarily bad--it can be a sign of good backward compatibility balanced against the need to improve core components. I think it is better than the alternative: multiple unofficial, de facto standard components that are incompatible. Who knows which direction each unofficial components will go and newcomers do not know which one to choose.
- Ygg2 3y agoThat only happened in Java 9 after Oracle acquired Sun. I'd say Oracle was way more remove focused than Sun ever was.
- thesuperbigfrog 3y agoYes. Oracle's goal for Java is to monetize it by reducing maintenance by divesting components to the community and focusing on the components that big organizations use (so they will pay for them). JavaFX was one of the many components that was spun out to the community and now lives as OpenJFX (https://openjfx.io/ https://openjfx.io/). JavaFX would have replaced Swing had it not been for Oracle's change of direction.
- cbm-vic-20 3y ago"OpenJFX is a project under the charter of the OpenJDK." Many committers are Oracle employees. https://github.com/openjdk/jfx https://github.com/openjdk/jfx
- thesuperbigfrog 3y ago>> "OpenJFX is a project under the charter of the OpenJDK." Many committers are Oracle employees. Yes, but while Oracle employees may contribute to OpenJFX development, JavaFX is not an officially supported Oracle product (beyond legacy support of old JDK versions): "Do I need a separate support contract for JavaFX? No. JavaFX is part of the technologies covered under Oracle Java SE Subscription. As of JDK 7u6 JavaFX is included with the standard JDK and JRE bundles. Note that for JDK 11 and later JavaFX is no longer included the JDK but remains available as a third party library from other vendors." Source: https://www.oracle.com/java/technologies/faqs-jsp.html https://www.oracle.com/java/technologies/faqs-jsp.html
- eviks 3y agoIn year 2078 you'll have a different language, so that's not an issue
- wokwokwok 3y agoLook... What if Arc and Rc weren't in the standard library, and you had to import them via a crate, and multiple different (incompatible) implementations existed such that you couldn't use them at the same time? Would that be ok? How about Option and Result? What if you could only use crates that used the same error library that you wanted to use? What about boxing and custom allocators? Can you imagine if different crates could opt into different allocators and you couldn't safely drop an object without passing it back to the crate it came from because 'who knows' what might happen if you try to deallocate it using your allocator? Should we not ship a default allocator and make that an optional thing too? ... That isn't a language I want to use. I'll take 'it comes with a default allocator' and that's good enough for me. If one day I get a stable 'you can pick, seamlessly at the top level, which allocator to use for your entire program', that's awesome! ...but it does not in any way mean, that I want a rust with no default allocator. The default allocator is great. It works perfectly for most things most people need, and it 'just works', out of the box, the first time you use rust. Async should work out of the box. It doesn't. That sucks.
- Ygg2 3y ago> How about Option and Result? This is somewhat of the case with many different Result/Error crates doing their own thing. So being in standard lib isn't a guarantee it won't fracture. > What if Arc and Rc weren't in the standard library, and you had to import them via a crate, and multiple different (incompatible) implementations existed such that you couldn't use them at the same time? First that's not currently what is happening in Rust. Second, I'd probably use the most popular and active one. Same as in JavaScript or Python. I think my criteria for what is in the standard lib is following: How often does the domain change? And should it come out of the box? E.g. are we inventing new ways to parse JSON? Yes ? Out of the standard lib you go. Is ARC/RC being reinvented? No? Go to standard lib.
- pwdisswordfishc 3y agoYou mean https://telepathy.freedesktop.org/ https://telepathy.freedesktop.org/ ?
- Ygg2 3y agoI meant brain to brain API using tachyons. As in particles not frameworks.
- thesuperbigfrog 3y ago>> I meant brain to brain API using tachyons. As in particles not frameworks. Just make sure you have a firewall. Fortunately there is Linux software for just this need: https://zapatopi.net/mindguard/ https://zapatopi.net/mindguard/ [JOKE]
- cozzyd 3y agothat's what https://lessemf.com/product/brain-coat/ https://lessemf.com/product/brain-coat/ is for
- geodel 3y ago> You'll have situation where in year 2078 someone will ask why are we still having tokio when everyone is using telepathy lib? Agree. This is why I refuse to use solar panels on roof. Science clearly tells sun is gonna burn down all its fuel and implode and at that point my solar roof will be useless deadweight.
- drpossum 3y agoI use rust as a case study about what happens when don't manage a need for users because of indecision and inflexibility. I've generally been disappointed by the rust community because of inflexibility and the unfortunate infighting that spills out. Just harmful to success.
- thinkharderdev 3y agoIs it harmful to success. It certainly seems like Rust has been wildly successful. Maybe the async fragmentation will change that but I don't see any evidence of that so far.
- j1elo 3y agoNot managing needs for lots of users because of indecision and/or inflexibility, has always been par for the course in Golang, as they wouldn't and won't introduce greatly requested features without years of careful study and design. And none of that has resulted in an adoption problem (but it indeed did result in a lot of whining). Actually, despite this extremely slow pace to introduce popular features, Go seems to be in very good shape.
- geodel 3y agoOne big difference is Go is primarily driven by Google devs and all the heavy duty work once agreed upon is implemented to last details by Google team. Rust is driven by volunteers for most part, so any carefully deliberated and designed things won't amount to much if implementers are busy, uninterested or just want to work other fun stuff and leave some things halfway done.
- ardfard 3y agoAren't the async situation in Rust is because the designer want Rust not to be opinionated and be flexible? i.e. you can choose not to have runtime in your app or using runtime that fit your particular needs.
- Ar-Curunir 3y agoWhat need does Rust not serve? You don't have to use async if you don't want to, and for most use cases Tokio suffices. The number of people who hit edge cases with using tokio with other libraries is small.
- tempodox 3y ago> What we have now is a circus. I couldn't agree more. And my conclusion is, as it has been, to stay away from async until we have a sane situation.
- reacharavindh 3y agoJust a bystander with a curious question.. Is it possible to avoid async with Rust when you use most common 3rd party libraries? such as ones to make API requests, database connectors, date/time, logging, deal with special kind of files etc.? or are we talking "the burden is on the user to set feature flags and carefully choose which crates they import into their projects"? Is it possible to set up the Rust toolchain to not allow async in a project at all?
- MuffinFlavored 3y ago> such as ones to make API requests, instead of reqwest you use ureq crate > database connectors can't answer at this time > date/time chrono crate has nothing to do with async > logging log crate with env_logger crate has nothing to do with async. pushing to something like elasticsearch instead of letting filebeat scrape your stdout is a different story > deal with special kind of files etc.? std::fs came first, the async stuff on top that recreate it in an async fashion came later. i'm pretty sure if you are dealing with a big file you can do std::fs with a "stream reader" basically
- reacharavindh 3y agoHmm. As a developer, how would one learn if you want no async, "instead of reqwest you use ureq crate"? unless they happen to search on HN first? Is there a way to tell Rust tool chain that async stuff is to be disabled, and importantly, is there a way to search the crate library with a filter for no async? Asking because I could only find a category for explicitly async crates, not the other way around.. https://crates.io/categories/asynchronous https://crates.io/categories/asynchronous
- 3y ago
- Rohansi 3y ago> how can you have one runtime that is multi-threaded and one runtime that is not, and expect to be able to seamlessly interchange them? That works fine for C#. When writing UI applications all async code started from the UI thread will continue to run single threaded on the UI thread. Everything else runs multi threaded in a thread pool. No need to change any of the async code to work for either.
- xnorswap 3y agoIIRC C# has management under the hood with different SynchronizationContext [0] to manage this, and it can lead to bad habits like sprinkling "ConfigureAwait(false)" all over code. It's also devilishly hard to understand, I've read a blog [1] on the subject several times and don't always fully grasp the consequences of different options. [0] https://learn.microsoft.com/en-us/dotnet/api/system.threading.synchronizationcontext?view=net-7.0 https://learn.microsoft.com/en-us/dotnet/api/system.threadin... [1] https://hamidmosalla.com/2018/06/24/what-is-synchronizationcontext/ https://hamidmosalla.com/2018/06/24/what-is-synchronizationc...
- pjmlp 3y agoNo it doesn't, hence why there are best practices guidelines written by the .NET architects, and there was a research project to add Go/Java co-routines as well. https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/blob/master/AsyncGuidance.md https://github.com/davidfowl/AspNetCoreDiagnosticScenarios/b... https://twitter.com/davidfowl/status/1532880744732758018?lang=en https://twitter.com/davidfowl/status/1532880744732758018?lan... https://github.com/dotnet/runtimelab/issues/2057 https://github.com/dotnet/runtimelab/issues/2057 https://github.com/dotnet/runtimelab/issues/2398 https://github.com/dotnet/runtimelab/issues/2398
- ko27 3y agoYes it does. Those best practices are very easy to follow and are enforced by analyzers. I have never encountered those issues on a recent big project I worked on, although they were common in the past when async was new. Also the green threads research concluded that it's not worth adding it to NET: https://github.com/davidwrighton/runtimelab/blob/report/docs/design/features/greenthreads.md#green-threads-technical-report https://github.com/davidwrighton/runtimelab/blob/report/docs...
- germandiago 3y agoHow does Rust async compare to C++20 coroutines in your opinion?
- pjmlp 3y agoCompared to .NET, Python and JavaScript async/await implementations, they both suck in the amount of boilerplate needed to implement, and debug async runtimes. As there is nothing being shipped in the box, both suffer from "go hunting" for runtimes, and the interoperability between them. On Windows, there are bonus complexity points, as they also get to interoperate with COM appartments, and OS async APIs.
- MrBuddyCasino 3y ago> An inconvenient truth about async Rust is that libraries still need to be written against individual runtimes. If there was a common standard, how do you resolve the additional need of sync/send+static for multithreaded executors?
- thinkharderdev 3y agoYou don't. Anything that can be sent between threads needs to be `Send` and anything shared between threads needs to be `Sync`. This is really important invariant that the rust compiler provides.
- yencabulator 3y agoSpec one API for singlethreaded and another for multithreaded executors.
- assbuttbuttass 3y ago> We should have an official runtime, officially managed, and guided by the same thoughts that guide the rest of the language. Agreed, it feels like we're in a worst-of-both-worlds situation. On one hand, tokio is relied upon by thousands of crates, and is very opinionated, meaning it's hard to innovate in the async space. On the other hand, tokio isn't a real standard, so we still get ecosystem fragmentation.
- wyager 3y agoI use at least 3 separate runtimes: tokio and 2 no_std runtimes (rtic and embassy). The latter would probably not be possible at all if there was an "official" runtime, because the official runtime would inevitably require allocation, and if it existed they wouldn't bother writing async in a flexible enough way that you could use it without an allocator. The way async is implemented in rust is actually technically quite impressive, and would almost certainly not exist if there were some official green thread solution. You could solve async/non-async polymorphism via the introduction of HKTs (and monads) - perhaps eventually they will be forced to do that. In the mean time, if they can make a few changes like stabilizing TAITs and async traits, that would go a long way to improving the ergos of async.
- zozbot234 3y ago> You could solve async/non-async polymorphism via the introduction of HKTs Rust has stabilized GATs, which are comparable in power to HKTs while having better interop with the language's broader feature set.
- wyager 3y agoI haven't thought about it super hard, but I suspect the ergos of that would be quite poor, as you would need to pass around the type of the trait object, even though all you really care about is the associated type constructor.
- karavelov 3y agoFor interop between runtimes, they need to add `std::async` IO traits that could be implemented by each runtime.
- thinkharderdev 3y agoAnd APIs for timers!
- weinzierl 3y agoNot sure if this is an apt comparison, but I like to think that the allocator is a good precedent. Similar to the async runtime most software needs one and most developers don't care much which one they use and are happy with the default allocator. Another similarity is that both are not just some ordinary old library but required by language features. We also usually don't use multiple ones in a single application. Still we allow the developer to choose an allocator or bring their own one.
- Groxx 3y agoNot being far enough into rust to know: is there a reason they can't settle on shared APIs? For single-threaded vs multi-threaded obviously you'll need to split the APIs, but why can't all 1/N-threaded runtimes work with all 1/N-threaded coroutines (and perhaps another split for no_std)? Ignoring historical differences of course - backwards compatibility means early ones probably can't ever work together. But the ecosystem isn't forever bound to those early implementations.
- thinkharderdev 3y agoI'm not sure there is any reason in principle but it doesn't work now because the APIs for various things are not really standardized. So for instance timers. If anywhere in my code I do `tokio::time::sleep(Duration::from_millis(100)).await` (which is quite common thing to do) then my code will no longer work with a non-tokio runtime.
- charcircuit 3y agoWhy wouldn't that work? The internal sleeping and calling the waker should not depend on the runtime.
- thinkharderdev 3y agoIt does. Under the hood it may just be creating a kernel timer but something needs to actually wake the task back up when the timer elapses, which is what the runtime does. The solution would probably just to create standard interfaces in std for this so you could just do `std::async::sleep(Duration::from_millis(100)).await` and just delegate the implementation details to the runtime (or something like that).
- Groxx 3y ago"You can wake up the sleeping loop" clearly depends on the runtime in that you have to contact the runtime to wake it up, but beyond that I don't really see it. Like, if I start a thread that calls `sleep(100); timer.resolve(); runtimeInstance.wake()` how is that related to the implementation details?
- surajrmal 3y agoI have managed to use async rust for over 4 years and never once use tokio. Primarily this is possible because I just avoid 3rd party libraries with async if they are tied to a particular async runtime. It is limiting, but I think it's important to be lean on 3rd party deps, so it's almost a good thing
- convolvatron 3y agoI'm very interested in this approach. sorry to be a pest, but could you point to the base traits/interfaces for using asynch without for example tokio? this might help me alot personally to get over some of my issues with rust. edit: is it just future/await and nothing else?
- cmrdporcupine 3y agoMy understanding is you always need a runtime to play the async game -- something needs to drive the async flow. But there are others on the market, just not without the.. market domination... of tokio. https://github.com/smol-rs/smol https://github.com/smol-rs/smol looks promising simply for being minimal https://github.com/bytedance/monoio https://github.com/bytedance/monoio looks potentially easier to work with than tokio https://github.com/DataDog/glommio https://github.com/DataDog/glommio is built around linux io_uring and seems somewhat promising for performance reasons. I haven't played with any of these yet, because Tokio is unfortunately the path of least resistance. And a bit viral in how it's infected things. But I'm planning on giving glommio at least a whirl.
- pests 3y agoThis is the Future impl: trait Future { type Output; fn poll(&mut self, wake: fn()) -> Poll<Self::Output>; } enum Poll<T> { Ready(T), Pending, } async functions get converted into -> impl Future<Output=original_return_type> automatically. You poll() until it returns Ready. wake() will notify you when it's ready to be polled again. That's it. Tokio and smol and these runtimes only exist to keep track of these, implement their own API, launch some threads, and run this event loop.
- saurik 3y ago> how can you have one runtime that is multi-threaded and one runtime that is not, and expect to be able to seamlessly interchange them? I feel like I do this in C++ right now without issue? I routinely mix/match coroutines from multiple runtimes, including one I built myself (which theoretically might could be multithreaded but very much right now is not and I know I rely on that still), one from cppcoro (which is a bit broken--I filed a bug with a detailed analysis, but it was never fixed--so I can only use a few parts that happen to add a lot of value), and one from boost asio (which is very much multithreaded and was a somewhat-impressive retrofit onto a more abstract design purely involving callbacks); I also effectively have a fourth, as another library I am using--libwebrtc--maintains its own I/O thread paradigm, and I have chosen to reinterpret a number of its delegation callbacks into coroutines. It involved some trivial adapters in a few places, but I developed those years ago and have long since forgotten as it all works so easily to willy-nilly co_await anything I want from wherever I am... is this really so difficult?
- thinkharderdev 3y agoIn rust it is yes because rust statically guarantees that you don't have data races (in safe rust at least). So you have marker traits `Send` and `Sync` which indicate that a type can be sent between threads (Send) or shared between threads (Sync) safely. So for a multi-threaded executor which can scheduler tasks on different threads when they resume has to make sure futures are `Send` whereas a single-threaded executor does not have that constraint.
- fiddlerwoaroof 3y agoAren’t the same data races possible in async without threads? As soon as you suspend one task and start another, you have the problem that the currently running task can break the invariants of the suspended one, regardless of whether you’re doing a single-threaded event loop or threads running in parallel.
- eximius 3y agoI think you'd need to be violating some other rule of rust to do that. e.g., single mutable access.
- bluejekyll 3y ago>> An inconvenient truth about async Rust is that libraries still need to be written against individual runtimes. >That's really the heart of it. If it was really just a runtime, it wouldn't matter what implementation you plugged in. It is absolutely possible to make a runtime agnostic library that can work over multiple runtimes. With the trust-dns libraries, we’ve managed to provide a resolver which is capable of working on async-std, Tokio (default), and even Fuchsia. It’s harder and takes planning, also to be fair and fully transparent we haven’t achieved this for all features, like DNS-over-quic. > We should have an official runtime, officially managed, and guided by the same thoughts that guide the rest of the language. I disagree. Rust is a systems level language capable of being used to build Operating Systems or other embedded tools, having a single runtime would make async Rust something you could not use in that context.
- geodel 3y agoRust situation reminds me of US military aphorism: “amateurs talk strategy and professionals talk logistics” Rust community is endlessly talking and obsessing with strategy where as average Rust user suffer from lack of logistics concerns about libraries / runtime usage etc.
- bluejekyll 3y agoMaybe you could express your concern differently? There are definitely a lot of ins-and-outs about many aspects of Rust. It operates differently from many other languages, sometimes in surprising ways. I agree that in some areas there could be better guidance. Is Tokio the runtime most people choose? Yes. Would most people be fine choosing that for their daily work? Yes. Might you want to choose a different one? It depends on what you’re doing, others have different goals and tradeoffs. Are there interface choices regarding things like Send + Sync or IO interfaces/traits you pick that will have impacts on how you structure your code to make it portable across runtimes? Absolutely. And finally, can Rust be better in regards to async development? Yes, everyone agrees that it should be. My big thing is that we really need async traits in the language. We have an excellent work around with the async-trait macro until we get support for it in the language, but you need to discover that, and then recognize some of its idiosyncrasies in certain situations.
- alex_lav 3y ago> What we have now is a circus. After 4 years of async being in stable. I stopped really paying attention to Rust about 5 years ago, and am asking purely out of ignorance/curiosity, but has the community/leadership approach changed much since then? I remember async being the Shiny New Future that was talked about a lot back then, but it certainly seems like what's been added has not really done well?
- rstuart4133 3y agoI don't understand why modern languages use "async" to do cooperative multitasking. Maybe someone can enlighten me. My (probably incorrect) understanding is that "async" arose from Javascript. It arose because pure event driven code is error prone and hard to get right compared to linear (stack based) code. The usual solution is threads, be they real or lightweight (aka cooperative multitasking) - but Javascript doesn't have threads and never will. Compared to pure event driven code using zillions of objects to save state, the pseudo stack based async solution is indeed a blessing. async is in effect a poor mans emulation of lightweight threads. It comes at the cost of needing language syntax to support it ("async" and "await") and it creates different colours of code, ie code that can't be mixed. The end result is parallel implementations of lots of libraries, leading to the situation the article and above comment both moan about. Lightweight threads / green threads achieve the same outcome as async, but without the downsides. No language extensions, no coloured code, all existing API's remain backward compatible. Javascript didn't have a choice, but why any language that does have a choice would use the async solution has me completely baffled. It's not like we didn't have numerous examples such as Elixir or Go, yet Rust went with async anyway.
- steveklabnik 3y ago> Maybe someone can enlighten me. I gave a talk (with transcript) about this here: https://www.infoq.com/presentations/rust-2019/ https://www.infoq.com/presentations/rust-2019/ > Lightweight threads / green threads achieve the same outcome as async, but without the downsides. This is not the case. Both have pros and cons.
- rstuart4133 3y ago> I gave a talk (with transcript) about this Thanks. A fascinating history. Do keep making them. One thing that had me scratching my head was the "green threads made C calls slower" comment. I don't understand why C would care where you call it from. > This is not the case. Both have pros and cons. Your talk highlighted on green thread con I hadn't though of. It hadn't occurred to me green threads introduced coloured code, just like async does. That was made plain by it needing a std::io implementation. But that isn't an additional con green threads have over async - it's a con they both share. While I get that green threads didn't interact with native threads very well, but I'm making a bet that was because they tried to hide the colours (different IO library) it needed, so the programmer didn't have to care. Async would have had the same problem had it tried to hid the colouring it introduces, but they solved that by not hiding it. Async warts over green threads of introducing a new syntax and a slightly different programming style remain.
- WuxiFingerHold 3y ago> What we have now is a circus. After 4 years of async being in stable. Hmmm, I wouldn't put it that way. There's Tokio as the defacto default runtime with a large ecosystem for "standard" usecases. Axum (Hyper, Tower middlewares) or Actix, SQLx/Diesel/Rust-Postgres, Request ... are a wonderful and for my limited usecases rich ecosystem.
- nextaccountic 3y agoThe problem is tokio only insofar as it blocks attempts to develop an unified, common denominator API between multiple runtimes (for example: how can the Rust ecosystem not yet have a standard async reader trait, after years and years?), and instead encourage all sorts of libraries to depend on tokio directly rather than a facade that works on multiple executors. Right now cross-runtime libraries are mostly written special-cased: one feature flag for tokio, another for async-std, maybe one for smol if they feel fancy. Almost none for glommio or other runtimes. That introduces a huge burden for libraries, that would rather depend on a single API. Rust shouldn't have an official runtime; it should have APIs that make possible to write libraries that don't dictate which runtime you must pick.
- dgroshev 3y agoThe main issue is that there is no consensus yet on how the API should look like. Considering Rust's backward and forward compatibility promises, committing to an API is an extremely serious step, all the more difficult when it's not clear what the API should be. Instead, Rust is waiting for patterns to emerge in the ecosystem, so that the universal API is compatible with all of them. It's a much safer route, but it also means waiting for the ecosystem to fragment. This is where we are now, but it's necessary and will get better over time. Edit: async readers/writers are a good example, because it seemed trivial to set in stone, except now we have io_uring that might require the API to move to the kernel instead of just holding &mut.