5 ms·
Once you get data races in Go, you realize that immutable data structures are indeed the right way. Too bad though. That non-threadsafe, racy map is your only g
by spion 11y ago
Once you get data races in Go, you realize that immutable data structures are indeed the right way. Too bad though. That non-threadsafe, racy map is your only generic data structure.
I think that critics are generally cutting way too much slack to Go. Its a horrible language - with a decent library and excellent tooling and documentation, but still quite horrible.
- AnimalMuppet 11y ago> Once you get data races in Go, you realize that immutable data structures are indeed the right way. False. There are more things in multi-threaded programming than are dreamt of in your philosophy. For some of them, immutability is very much the wrong way. It means some threads will be playing with stale data for some time. I'm not saying that I'd want to write that kind of a program in Go, mind you. But your over-generalization is blatantly false. And, in fact, I suspect that most people writing in Go aren't writing the kind of program where you could get data races at all. Multithreaded programming in Go (if I understand correctly) is mostly a matter of handling multiple independent data streams, not threads that need to access the same data objects. I suspect that your over-generalization in the second paragraph is equally false, but I have less experience to validate that opinion.
- spion 11y agoEven if we assumed that (but seriously though, I invite you to at least provide one valid, complete example) your argument is valid, what you're saying is that Go is good for that subset of cases ("some of them") where immutability is the wrong way. Does not invalidate the fact that it has zilch to offer for the cases where its the right way.
- AnimalMuppet 11y agoFirst: I didn't say that Go was good for that subset of cases. In fact, I said that I wouldn't want to write that kind of program in Go. I said that Go wasn't the wrong answer for the reason you stated, namely immutable data. You want an example? Here's a video router for a TV station, which has multiple sources of user input (human-pushable control panels on two different data buses, plus serial data coming from multiple automation systems). You need to keep those control panels and automation systems updated with what's connected to what, even if they weren't the source of the command that changed it. And you need to keep the actual hardware switch up to date, too. And commands to the switch can fail, which you need to report back to whoever made the command. (One way the command can fail is if someone else locked an output to display a particular input.) Faced with that problem, we implemented a single "state of the switch" data object that mutated as commands came in. But you could think about trying to implement it with immutable data. That would mean creating a new copy of the state of the switch for each (successful) command that was processed. That would mean copying a fairly large chunk of data many times a second, which would have been a challenge for the processor we had. That would also almost certainly mean a garbage-collected language which, when you're trying to respond within one TV frame (1/60th of a second), is a really bad idea. More to the point for our discussion, it would also mean that threads, which in our design only had to respond to one source of control, would now also have to handle state-of-the-switch updates pushed to them from other threads (or from some master). That seems like significant additional complexity to me. (Yes, I know that data races have their own complexity, but for our design, it was very clear how to prevent that. And if you're going to say that we could have had a separate thread receive the updates to update the control panels, now we've got a race as to who owns the hardware control bus to the panels.)
- spion 11y agoNow thats a good example! First, a little correction: you don't have to make a copy of the entire state. This video explains the trick on how to get immutable data structures that share most of their data with their previous version https://youtu.be/SiFwRtCnxv4?t=8m39s https://youtu.be/SiFwRtCnxv4?t=8m39s - list are straightforward, and vectors and maps are based on the same HAMT tree-like structure. Of course, a system with real time constrains and hardware limitations will have different optimal solutions. And yes, reference counting is the bare minimum you'd probably like for these structures (GC is even better) However, we're talking about Go here - a language designed for writing servers that has a GC. The solution in Haskell is actually quite nice: MVars [1] plus immutable data structure. An MVar contains the current state, represented by one such structure. takeMVar "removes" the variable - a thing which can be done atomically by the updating thread when the data becomes stale. After that, subsequent attempts to readMVar from other threads would block until there is a new updated value, to ensure everything is in sync. Finally, the updating thread does a putMVar, and all readers get the new value and continue executing. The best part is they don't have to worry that the updating thread might start another update in parallel while they read: the data structures are immutable so the value being read is guaranteed to remain immutable. Even if the updating thread continues "modifying" the new structure in the background, it doesn't have an effect on the other consumer's version. But yeah, all this is pointless if you have realtime constraints and therefore need super-tight control over execution time. It might be doable in a fast reference counted language, but it will also be much harder to reason about the time it will take to release the memory for the segments that aren't in use anymore. [1]: https://hackage.haskell.org/package/base-4.8.1.0/docs/Control-Concurrent-MVar.html https://hackage.haskell.org/package/base-4.8.1.0/docs/Contro...
- AnimalMuppet 11y ago> The best part is they don't have to worry that the updating thread might start another update in parallel while they read: the data structures are immutable so the value being read is guaranteed to remain immutable. Even if the updating thread continues "modifying" the new structure in the background, it doesn't have an effect on the other consumer's version. If I understand what you said here correctly, this doesn't work for my example. A thread cannot continue with a stale version (and function properly). It must operate on a current version all the time (or block until it can).
- burntsushi 11y ago> you realize that immutable data structures are indeed the right way Well, that's false. Rust has mutable data structures but also statically prevents data races.
- spion 11y agoDo you mean this? https://doc.rust-lang.org/nomicon/races.html https://doc.rust-lang.org/nomicon/races.html If that then sure, that might work too if you really need to do it. I wouldn't go so far as to say its the right way - it seems very bothersome to me. Its more of a "yes, I'm willing to go through all this incredible pain, because I get some gain for it (constrained hardware? idk). Rust, please help me do it right." Of course you can't do that in Go. Not only you don't get static guarantees, you can't even write a generic map with atomic access. edit: by pain I mean this: https://doc.rust-lang.org/book/concurrency.html https://doc.rust-lang.org/book/concurrency.html - and yes, thats painful compared to using immutable data structures.
- burntsushi 11y ago> If that then sure, that might work too if you really need to do it. I wouldn't go so far as to say its the right way - it seems very bothersome to me. There are plenty of bothersome things about "immutable only" too. I've employed both approaches in earnest. Each have their own set of trade offs. > Of course you can't do that in Go. Not only you don't get static guarantees, you can't even write a generic map with atomic access. You have to remember to use RWMutex every single time. No generics. I'm quite aware of Go's limitations, thanks. I find it amusing that you've dismissed an entire category of practice to statically eliminating data races at barely a glance. Irony, it seems. The very thing that people lament about Gophers is precisely the behavior you've demonstrated here! (Quite literally in fact. How many gophers have you heard say something like "generics is bothersome"?) You've been polite, but snobbery is vexing, no matter where it comes from.
- spion 11y agoFair enough. I admit to not knowing when you would prefer statically checked mutable data structures to immutable ones except for a few cases (dynamic programming arrays, fast matrix libraries, memory constrained environments). I did use the word "seems" there though. Its not really dismissal, I would indeed like to be enlightened. In projects where I can afford a GC, I'd always take the GCed option (in my case an overwhelming majority). Same for immutable data structures (use whenever they can be afforded). Are those bad heuristics? (Afforded here refers to performance/memory constraints only) One thing that GCed languages don't solve very well is handling other more scarce resources (file handles, connections from a pool, etc). It seems that Rust managed to solve this nicely. If only it was possible to use GC for everything except those kinds of resources (perhaps it is?), that would be perfect.