4 ms·
I've been doing async Rust since the tokio 0.x combinator days, and I remember being like OP. For some reason `Arc` did not exist in my mind and it was a strugg
by nemothekid 4y ago
I've been doing async Rust since the tokio 0.x combinator days, and I remember being like OP. For some reason `Arc` did not exist in my mind and it was a struggle appeasing the borrow checker.
The Go version is similar to the final Rust version; except the Go version is forcing you to use Arc everyone[1]. Seriously, just use Arc (or Arc<Mutex<>>), in 70% of cases you are wrestling with the borrow checker trying to do something dangerous, in the other 30% the borrow checker is wrong and isn't smart enough to understand what you are trying to do. In both cases, 95% of the time, you aren't creating enough objects per second to justify getting rid of that atomic add.
I'm not even sure this is limited to async code either. I've seen plenty of code by C++ "zero cost" gods which abuse lifetimes in order to avoid a single clone or Arc. At least the compiler made sure you won't segfault, but I'm not sure the complexity is worth it over just an Arc.
[1] I know this this is a simplification.
- Hirrolot 4y agoI do completely agree with your advice of using `Arc`s. But unfortunately, sometimes it gets very tedious to deal with all these `Arc`s: https://github.com/teloxide/teloxide/blob/ec1d41220c51872cf96e597072c63cdfe77c3521/src/dispatching/dispatcher.rs#L418 https://github.com/teloxide/teloxide/blob/ec1d41220c51872cf9.... It also blurs the advantage of Rust as a lifetime validation tool if you use reference counting anyways. But it seems that it's the only viable approach for async at the moment.
- nemothekid 4y agoI have plenty of code like that: let id2 = id.clone(); let watch_id2 = watch_id.clone(); let id3 = id.clone(); let watch_id3 = watch_id.clone(); let ready2 = Arc::downgrade(&ready); let video_stream = async move { // use id2, watch_id2, id3, watch_id3, ready2 } My realization is that in most cases an Arc is the right tool for the job, especially in concurrent applications. I see it as the borrow checker doing it's job, if you have two threads that need to share data and one of them may go away at any moment, the only way you can ensure the memory stays around is with "runtime borrow checking", e.g. a garbage collector. In C++, the way this is solved is usually "just trust me, I won't use this memory here"
- cormacrelf 4y agoThe overall effect of Arcs being more usable is something that was true before Rust. It is an emergent property of Rust too, because it's still true. The alternative to using Arc or similar used to be validating your own lifetimes manually and inevitably making show-stopping mistakes in C/C++. Now the alternative is machine-checking it and being able to never make mistakes. But Arc is still more ergonomic than either. If Rust forces you to use Arc where in C/C++ you would normally just be freewheeling your lifetimes, by complaining about your lifetimes, a lot of people are going to see that as "Rust is really annoying and gets in my way, it's making me choose between a tangle of lifetime bounds and explicit Arcs". The truth is that it is teaching you that your normal style would have led to mistakes and you should never have been coding that way. So people go on complaining about being re-taught until their normal style works in Rust, or until they give up. (I don't think that's what you're doing, this is mainly just to say Arcs are not a waste of the language's power, rather an effect of the language's power to know when your freewheeling would be mere lifetime guesstimation and quite possible buggy in C++.) The author's point is strong though. > the process of designing APIs is affected by numerous arbitrary language limitations like those we have seen so far. There's a long way to go in being able to perfectly express all the ways you might want to use lifetimes. Sometimes you come across a situation when a few of these limitations converge on one spot, and you get the author's problem. But equally, the author is trying to write something that has probably never had its lifetime rules described before. Yes, languages with GCs can do it easily. That was also true of lock-free queues & running destructors. GCs have it easy in a ton of different areas around lifetimes. The core complaint here is that Rust does not have a GC and we have had it really easy being able to rely on one & not having to write down exactly the lifetime bounds that could make it work statically. The author is complaining that there are hundreds of compiler issues preventing some of this being expressed... well, yeah, nobody has ever, EVER, tried to write this stuff down before in a machine-readable way, we are slowly covering the untrodden ground, and you might be in an especially untrodden area. I think that's a pretty good excuse for the compiler having issues and some things being not quite expressible, but it's also an important caveat to "fearless concurrency" and I think the post illustrated that well.
- klabb3 4y ago"If you can't handle me at my BoxFuture, Arc<Mutex<_>> and #[async_trait], you don't deserve me at my zero cost abstractions and fearless concurrency."
- yencabulator 4y ago> except the Go version is forcing you to use Arc everyone The overhead of atomic reference counts everywhere is higher than the overhead of a good garbage collector. This thinking is biased against Go for no reason.
- nemothekid 4y ago>[1] I know that this is a simplification. I don't think it's biased against Go. The an `Arc` (or garbage collector) is the correct way to model lifetimes for complex programs whose lifetimes are managed at runtime rather than compile time. In the context of both languages, rarely is overhead of an atomic add or GC going to be the limiting factor of your programs, just like Java programs are efficient. While static memory management is nice, it's not the sole reason I continue to use Rust. I've been using Go since 1.0, and there are two times where I have ever thought "My program is slow because of GC", and in one of those times, it was solved by simply upgrading Go.