12 ms·
I find myself in this weird corner when it comes to async rust. The guy's got a point in that doing a bunch of Arc, RwLock, and general sharing of state is goi
by lordnacho 3y ago
I find myself in this weird corner when it comes to async rust.
The guy's got a point in that doing a bunch of Arc, RwLock, and general sharing of state is going to get messy. Especially once you are sprinkling 'static all over the place, it infects everything, much like colored functions. I did this whole thing once back when I was starting off where I would Arc<RwLock> stuff, and try to be smart about borrow lifetimes. Total mess.
But then rust also has channels. When you read about it, it talks about "messages", which to me means little objects. Like a few bytes little. This is the solution, pretty much everything I write now is just a few tasks that service some channels. They look at what's arrived and if there's something to output, they will put a message on the appropriate channel for another task to deal with. No sharing objects or anything. If there's a large object that more than one task needs, either you put it in a task that sends messages containing the relevant query result, or you let each task construct its own copy from the stream of messages.
And yet I see a heck of a lot of articles about how to Arc or what to do about lifetimes. They seem to be things that the language needs, especially if you are implementing the async runtime, but I don't understand why the average library user needs to focus so much on this.
- MuffinFlavored 3y agohow do you do bidirectional channels/rpc? like “send request to channel A with message 123, make sure to get a response back from channel B exactly for that message”
- artursapek 3y agothis is what Go got right like 10 years ago
- dralley 3y agoIn the sense that green threads are easier, sure. But green threads were not and are not the right solution for Rust, so it's kind of beside the point. Async Rust is difficult, but it will eventually be possible to use Async Rust inside the Linux kernel, which is something you can't do with the Go approach.
- anonymoushn 3y agoMaybe "add a runtime that switches execution contexts on behalf of the user" and "force the programmer to reimplement everything" are not the only options.
- artursapek 3y agoin the sense that sharing memory by communicating is the right approach
- bojo 3y agoI think they are referring to channels, which came with the tagline "share memory by communicating."
- artursapek 3y agoyes
- kibwen 3y agoRust has had channels since before Go was even publicly announced. Remember that Rust, like Go, was inspired by Pike's earlier language Limbo, which uses CSP. https://en.wikipedia.org/wiki/Limbo_(programming_language) https://en.wikipedia.org/wiki/Limbo_(programming_language)
- Pauan 3y agoRust has had OS channels since forever, and async channels for 5 years. Rust has changed a lot in the past 5 years, people just haven't noticed, so they assume that Rust is still an old outdated language.
- KRAKRISMOTT 3y agoWe need a way to bridge the gap. Having a runtime may not be suitable for all apps but it can easily allow you to reach 95%+ concurrency performance. The async compile-to-state-machine model is only necessary for the last 5%. Most userland apps rarely need to maximize concurrency efficiency. They need concurrency yes, but performance at the 95th percentile is more than sufficient.
- raincole 3y agoGo: it turns out that generic is actually useful Rust: it turns out that not every concurrency needs to be zero-cost abstraction
- tumdum_ 3y agoExcept that Rust hasn’t yet realised it
- vacuity 3y agoIf Rust had gone with green threads as the core async strategy (I know it was a thing pre-1.0), that would be terrible. You're not understanding Rust's design goals. Rust's async model, while it has several major pain points at present, is still undoubtedly superior for what Rust was made for. It would be a shame to throw all that away. Go can go do it's own thing (it has, evidently).
- mrkline 3y agoHoare Was Right. (But if you're only firing up a few tasks, why not just use threads? To get a nice wrapper around an I/O event loop?)
- tel 3y agoWaiting asynchronously on multiple channels/signals. Heterogenous select is really nice.
- mrkline 3y agoIt's great! But there's nothing about it that requires futures. It really annoys me that something like this isn't built-in: https://github.com/mrkline/channel-drain https://github.com/mrkline/channel-drain
- tel 3y agoThat works for channels, but being able to wait other asynchronous things is better. Timeouts for instance. We could imagine extending this to arbitrary poll-able things. And now we have futures, kind of.
- simias 3y agoIt really is, but I still favour "unsexy" manual poll/select code with a lot of if/elseing if it means not having to deal with async. I fully acknowledge that I'm an "old school" system dev who's coming from the C world and not the JS world, so I probably have a certain bias because of that, but I genuinely can't understand how anybody could look at the mess that's Rust's async and think that it was a good design for a language that already had the reputation of being very complicated to write. I tried to get it, I really did, but my god what a massive mess that is. And it contaminates everything it touches, too. I really love Rust and I do most of my coding in it these days, but every time I encounter async-heavy Rust code my jaw clenches and my vision blurs. At least my clunky select "runtime" code can be safely contained in a couple functions while the rest of the code remains blissfully unaware of the magic going on under the hood. Dear people coming from the JS world: give system threads and channels a try. I swear that a lot of the time it's vastly simpler and more elegant. There are very, very few practical problems where async is clearly superior (although plenty where it's arguably superior).
- cratermoon 3y ago> But then rust also has channels. When you read about it, it talks about "messages", which to me means little objects. Like a few bytes little. As a wise programmer once said, "Do not communicate by sharing memory; instead, share memory by communicating"
- taneq 3y agoOoh, that's very ezn. Ah crap I think I have a race condition.
- Cu3PO42 3y agoI really like the message passing paradigm. And languages like Erlang have shown that its an excellent choice... for distributed systems. But writing code like that is a very diffferent experience from, say, async JavaScript, which feels more like writing synchronous code with green threads (except you have to deal with function coloring as well). I believe people will try to write code in a way that is already familiar to them, leading them down the path of Arc and RwLock in Rust.
- rkangel 3y ago> But writing code like that is a very diffferent experience from, say, async JavaScript, I write a fair amount of code in Elixir professionally and this isn't how I view it. There are some specific Elixir/Erlang bits of ceremony you need to do to set up your supervision tree of GenServers, but then once that's done you get to write code that feels like so gle threaded "ignore the rest of the world" code. Some of the function calls you're making might be "send and message and wait for a response" from GenServers etc. but the framework takes care of that. I wrote some driver code for an NXP tag chip. Driving the inventory process is a bit involved, you have to do a series of things, set up hardware, turn on radio, wait a bit, send data, service the SPI the whole time in parallel. With the right setup for the hardware interface I just wrote the whole thing as a sequence, it was the simplest possible code you could imagine for it. And this at the same time as running a web server, and servicing hardware interrupts that cause it to reload the state of some registers and show them to each connected web session.
- radlad 3y agoGo also uses goroutines and channels to facilitate message passing, or as they describe it, "sharing memory by communicating." I imagine Rust to be a language far more similar to Go, in both use cases and functionality, than JS.
- Cu3PO42 3y ago> I imagine Rust to be a language far more similar to Go, in both use cases and functionality, than JS. I mostly agree. But I would wager that for a significant amount of people their first exposure to "async" is JS and not any number of other languages. And when you try to write async Rust the same way as you might write async JS, things just aren't that pretty.
- mananaysiempre 3y ago> If there's a large object that more than one task needs, either you put it in a task that sends messages containing the relevant query result, or you let each task construct its own copy from the stream of messages. When you can guarantee sole ownership, why not put that exclusive pointer in the message? I’d think that this sort of compile-time lock would be an important advantage for the type system. (I think some VMs actually do this sort of thing dynamically, but I can’t quite remember where I read about it.) On a multiprocessor, there’s of course a balance to be determined between the overhead of shuffling the object’s data back and forth between CPUs and the overhead of serializing and shuffling the queries and responses to the object’s owning thread. But I don’t think the latter approach always wins, does it? At least I can’t tell why it obviously should.
- fnordpiglet 3y agoI find the criticisms a little strange - async doesn’t imply multithreaded, and you don’t need to annotate everything shared with magic keywords if you’re async within the same thread because there’s no sharing. Only one future at a time is running on the thread and they’re within the same context. When moving between threads I do what you suggest here and use channels to send signals rather than having a lot of shared state. Sometimes there is a crucial global state something that’s easier to just directly access, but I just write struct that manages all the Arc/RwLock or whatever other exclusive access mechanism I need for the access patterns. From the callers point of view everything is just a simple function call. When writing the struct I need to be thoughtful of sharing semantics but it’s a very small struct and I write it once and move on. I also don’t understand their concern about making things Send+Sync. In my experience almost everything is easily Send+Sync, and things that aren’t shouldn’t or couldn’t be. I get that sometimes you just want to wear sweatpants and write code without thought of the details, but most languages that offer that out of the box don’t really offer efficient concurrency and parallelism. And frankly you rarely actually need those things even if the “but it’s cool” itch is driving you. Most of the time a nodejs-esque single threaded async program is entirely sufficient, and a lot of the time Async isn’t even necessary or particularly useful. But when you need all these things, you probably need to hike up your sweatpants and write some actual computer code - because microseconds matter, profiled throughput is crucial, and nothing in life that’s complex is easy and anyone selling you otherwise is lying.
- brundolf 3y ago> Sometimes there is a crucial global state something that’s easier to just directly access, but I just write struct that manages all the Arc/RwLock or whatever other exclusive access mechanism I need for the access patterns. From the callers point of view everything is just a simple function call. When writing the struct I need to be thoughtful of sharing semantics but it’s a very small struct and I write it once and move on. This is a recurring pattern I've started to notice with Rust: most things that repeatedly feel clunky, or noisy, or arduous, can be wrapped in an abstraction that allows your business logic to come back into focus. I've started to think this mentality is essential to any significant Rust project.
- satvikpendem 3y ago> But then rust also has channels. When you read about it, it talks about "messages", which to me means little objects. Like a few bytes little. This is the solution, pretty much everything I write now is just a few tasks that service some channels. They look at what's arrived and if there's something to output, they will put a message on the appropriate channel for another task to deal with. No sharing objects or anything. If there's a large object that more than one task needs, either you put it in a task that sends messages containing the relevant query result, or you let each task construct its own copy from the stream of messages. The dream of Smalltalk and true OOP is still alive.
- otabdeveloper4 3y agoOOP was a shit idea that needed to die.
- satvikpendem 3y agoThere's a difference between Smalltalk's concept of OOP and Java's. I'm not talking about Java's.
- macintux 3y agoErlang often crops up in these conversations.
- bluGill 3y agoWhy does Smalltalk constantly get credit for being true OOP? Simula was doing OOP long before Smalltalk. Most languages choose Simula style OOP, and reject the things that make Smalltalk different. If you say Smalltalk is better OOP I might agree, but calling it "true" is not correct.
- KMag 3y agoAlan Kay is generally credited with coming up with the term "object-oriented", so for better or for worse, many people defer to his definition and his embodiment of ideas when looking for a strict definition of the term.
- harpiaharpyja 3y agoI remember picking up this sort of advice from a professor way back in college. It's a godsend. Structure the problem as data flowing between tasks and connect them up with queues, avoid sharing state. It's just a better way to deal with multithreading no matter what language you use.
- brigadier132 3y agoAsync is not a solution for data parallelism.
- bluGill 3y agoThere is a time any place for sharing state and data. However it is extremely complex to make that work, and so if at all possible don't. In general the only time I can't use queues is when I'm writing the queue implementation (I've done this several times - turns out there are a number of different special cases in my embedded system where it was worth it to avoid some obscure downside to the queues I already had). When you need the absolute best performance sharing state is sometimes better - but you need a deep understanding of how your CPUs share state. A mutex or atomic write operation is almost always needed (the exceptions are really weird), and those will kill performance so you better spend a lot of time minimizing where you have them.
- samsquire 3y agoI like this too. I would also suggest looking into ringbuffers and LMAX Disruptor pattern. There is also Red Planet Lab's Rama, which takes the data flow idea and uses it to scale.
- Animats 3y agoThat was the argument for Go. But Go is not used that way. People still share and lock stuff in Go. Go is only safe for race conditions that break the memory model, not all race conditions, as Rust is.
- skitter 3y agoTechnically, you can break Go's memory model via race conditions: Write to an interface on one thread while reading it from another and you may read the old vtable pointer but new data pointer / the other way around. Same goes for slices with data/length/capacity.
- sa46 3y agoRust protects against data races, not race conditions. https://doc.rust-lang.org/nomicon/races.html https://doc.rust-lang.org/nomicon/races.html
- Ar-Curunir 3y agoGo doesn’t protect against even those, which is what the parent meant
- sa46 3y agoGo cannot catch data races at compile time (like Rust) but can catch a subset of data races at run time with the race detector. Go provides imperfect, opt-in protection.
- spockz 3y agoChannels with passing messages has been around as a solid way for doing async and multi threading since forever. These systems are called actor based systems. Erlang is a good example which uses it as its core. Then on the jvm there is Akka. Axon is another.
- modeless 3y agoYou don't need Rust for that. You can even do it in JavaScript.
- brabel 3y agoThe author does mention that you should probably stop at using Threads and passing data around via channels... but then mentions the C10K problem and says that sometimes you need more... but does not answer the question that I think is begging to be asked: does using Rust async with all the complications (Arc, cloning, Mutex whatever) does actually outperform Threads/channels?? Even if it does, by how much? It would be really interesting to know the answer. I have a feeling that Threads/channels may be more performant in practice, despite the imagined overhead.
- Pauan 3y agoThreads cannot scale at all, because you're limited to the number of threads (which is usually quite small). Async code can scale essentially infinitely, because it can multiplex thousands of Futures onto a single thread. And you can have millions of Futures multiplexed onto a dozen threads. This makes async ideal for situations where your program needs to handle a lot of simultaneous I/O operations... such as a web server: http://aturon.github.io/blog/2016/08/11/futures/ http://aturon.github.io/blog/2016/08/11/futures/ Async wasn't invented for the fun of it, it was invented to solve practical real world problems.
- vacuity 3y agoThreads, at least on Linux, are much more lightweight than you seem to think. Async Rust can scale better, of course, but you're overexaggerating your case.
- mrfox321 3y agoIf your system cannot be decomposed away from shared mutable state, then you cannot avoid lifetime management and synchronization primitives. Ultimately, it depends on your data model.
- westurner 3y agoThere's not a good distributed concurrent benchmark in the Techempower Web Framework benchmarks, because the Multiple Queries and Fortunes test programs don't use any parallelism or concurrency primitives to win at fast SQL queries. https://www.techempower.com/benchmarks/#section=data-r21&test=query https://www.techempower.com/benchmarks/#section=data-r21&tes... From https://news.ycombinator.com/item?id=37289579 https://news.ycombinator.com/item?id=37289579 : > I haven't checked, but by the end of the day, I doubt eBPF is much slower than select() on a pipe()? Channels have a per-platform implementation. - "Patterns of Distributed Systems (2022)" (2023) https://news.ycombinator.com/item?id=36504073 https://news.ycombinator.com/item?id=36504073