Remix.run Logo
MORPHOICES 20 hours ago

How do you actually learn concurrency without fooling yourself?

Every time I think I “get” concurrency, a real bug proves otherwise.

What finally helped wasn’t more theory, but forcing myself to answer basic questions:

What can run at the same time here?

What must be ordered?

What happens if this suspends at the worst moment?

A rough framework I use now:

First understand the shape of execution (what overlaps)

Then define ownership (who’s allowed to touch what)

Only then worry about syntax or tools

Still feels fragile.

How do you know when your mental model is actually correct? Do you rely on tests, diagrams, or just scars over time?

tetha 16 hours ago | parent | next [-]

I've written, tested and debugged low-level java concurrency code involving atomics, the memory safety model and other nasty things. All the way down to considerations if data races are a problem or just redundant work and similar things. Also implementing coroutines in some complang-stuff in uniersity.

This level is rocket science. If you can't tell why it is right, you fail. Such a failure, which was just a singular missing synchronized block, is the _worst_ 3-6 month debugging horror I've ever faced. Singular data corruptions once a week on a system pushing millions and trillions of player interactions in that time frame.

We first designed with many smart people just being adverse and trying to break it. Then one guy implemented, and 5-6 really talented java devs reviewed entirely destructively, and then all of us started to work with hardware to write testing setups to break the thing. If there was doubt, it was wrong.

We then put that queue, which sequentialized for a singular partition (aka user account) but parallelized across as many partitions as possible live and it just worked. It just worked.

We did similar work on a caching trie later on with the same group of people. But during these two projects I very much realized: This kind of work just isn't feasible with the majority of developers. Out of hundreds of devs, I know 4-5 who can think this way.

Thus, most code should be structured by lower-level frameworks in a way such that it is not concurrent on data. Once you're concurrent on singular pieces of data, the complexity explodes so much. Just don't be concurrent, unless it's trivial concurrency.

brabel 2 hours ago | parent | next [-]

That’s why the actor model is so good. You have concurrent programs but each Actor has full ownership of its data and can access it as if it were single threaded. In my opinion, it’s the only way to get it right and should only be replaced with low level atomics if performance proves to be much better and that impacts the business strongly, which I have never seen in practice.

xenihn 14 hours ago | parent | prev [-]

I'm interested in knowing more details about this if you happen to have a post written up somewhere!

jesuslop 18 hours ago | parent | prev | next [-]

Heisembugs aren't just technical debt but project killer time bombs so one must better have a perfect thread design in head that works first attempt, else is hell on earth. I can be safe in a bubble world with whole process scope individual threads or from a thread pool (so strong guarantees of joining every created thread) and having share-nothing threads communicating only by prosumer sync-queues that bring a clear information-flow picture. One can have a message pump in one thread, as GUI apps do. That is just a particular case of the prosumer channel idea before. Avoid busy waits, wait on complex event conditions by blocking calls to select() on handler-sets or WaitForMultipleObjects(). Exceptions are per thread, but is good to have a polite mechanism to make desired ones to be potentially process-fatal, and fail earliest. This won't cover all needs but is a field-tested start.

19 hours ago | parent | prev | next [-]
[deleted]
mrkeen 19 hours ago | parent | prev [-]

Share xor mutate, that's really all there is

ragnese 18 hours ago | parent [-]

Talk about trivializing complexity...

The idea that making things immutable somehow fixes concurrency issues always made me chuckle.

I remember reading and watching Rich Hickey talking about Clojure's persistent objects and thinking: Okay, that's great- another thread can't change the data that my thread has because I'll just be using the old copy and they'll have a new, different copy. But now my two threads are working with different versions of reality... that's STILL a logic bug in many cases.

That's not to say it doesn't help at all, but it's EXTREMELY far from "share xor mutate" solving all concurrency issues/complexity. Sometimes data needs to be synchronized between different actors. There's no avoiding that. Sometimes devs don't notice it because they use a SQL database as the centralized synchronizer, but the complexity is still there once you start seeing the effect of your DB's transaction level (e.g., repeatable_read vs read_committed, etc).

mrkeen 18 hours ago | parent [-]

It's not that shared-xor-mutate magically solves everything, it's that shared-and-mutate magically breaks everything.

Same thing with goto and pointers. Goto kills structured programming and pointers kill memory safety. We're doing fine without both.

Use transactions when you want to synchronise between threads. If your language doesn't have transactions, it probably can't because it already handed out shared mutation, and now it's too late to put the genie in the bottle.

> This, we realized, is just part and parcel of an optimistic TM system that does in-place writes.

[1] https://joeduffyblog.com/2010/01/03/a-brief-retrospective-on...

ModernMech 17 hours ago | parent [-]

+5 insightful. Programming language design is all about having the right nexus of features. Having all the features or the wrong mix of features is actually an anti-feature.

In our present context, most mainstream languages have already handed out shared mutation. To my eye, this is the main reason so many languages have issues with writing asynch/parallel/distributed programs. It's also why Rust has an easier time of it, they didn't just hand out shared mutation. And also why Erlang has the best time of it, they built the language around no shared mutation.