Hacker News new | ask | show | jobs
by jerf 987 days ago
"Channels are nice, until they are not. I feel as though it's easier, though not necessarily easy, to write correct programs using mutexes than Go channels in many cases."

The rule for mutexes is, never take more than one. As long as you only ever take one, life is pretty good.

When all you had was mutexes as your primitive, though, that became a problem. One is not enough. You can't build a big program on mutexes, and taking only one at a time.

But as you add other concurrency primitives to take the load off of the lowly mutex, and as you do, the mutex returns to viability. I use a lot of mutexes in my Go code, and I can, precisely because when I have a case where I need to select from three channels in some complicated multi-way, multi-goroutine choice, I have the channels for that case. The other concurrency mechanisms take the hard cases, leaving the easy cases for mutexes to be fine for once again.

The story of software engineering in the 1990s was a story of overreactions and misdiagnoses. This was one of them. Mutexes weren't the problem; misuse of them was. Using them as the only primitive was. You really, really don't want to take more than one at a time. That goes so poorly that I believe it nearly explains the entire fear of multithreading picked up from that era. (The remainder comes from trying to multithread in a memory-unsafe language, which is also a pretty big mistake.) Multithreading isn't trivial, but it isn't that hard... but there are some mistakes that fundamentally will destroy your sanity and trying to build a program around multiple mutexes being taken is one of them.

(To forestall corrections, the technical rule is always take mutexes in the same order. I consider experience to have proved that doesn't scale, plus, honestly, just common sense shows that it isn't practical. So I collapse that to a rule: Never have more than held at a time. As soon as you see you need more than one, use a different mechanism. Do whatever it takes to your program to achieve that; whatever shortcut you have in mind that you think will be good enough, you're wrong. Refactor correctly.)

5 comments

> The rule for mutexes is, never take more than one. As long as you only ever take one, life is pretty good.

Is there any shortcoming you can't apply that to? Don't malloc unless you free. If you cast in your program, make sure to cast to the correct type.

> The story of software engineering in the 1990s was a story of overreactions and misdiagnoses. This was one of them.

The problem of multiple mutexes was diagnosed well before the 90s. "Dining philosophers" was formulated in 1965.

    https://www.adit.io/posts/2013-05-11-The-Dining-Philosophers-Problem-With-Ron-Swanson.html

    https://www.adit.io/posts/2013-05-15-Locks,-Actors,-And-STM-In-Pictures.html
Yeah, I can relate to this. When coding in Go I routinely combine mutexes and channels. Channels are useful for signalling and some other things. I am a bit miffed though since it absolutely feels like CSP concurrency and message passing COULD be better, it's just not in Go. Maybe I'll try Erlang some day and be imbued with the curse of knowledge.

Rust doesn't solve the problem of multiple mutexes being tricky, but it does at least solve most of the other problems with sharing memory. To gain a little more assurance with Go, I do sometimes use the checklocks analyzer from gVisor, which gets you some of the way.

"Maybe I'll try Erlang some day and be imbued with the curse of knowledge."

For the purposes of this discussion [1], Erlang is just Go except you don't have mutexes as an option at all, so anything you want locked has to be in a separate Erlang process (analog of goroutine). So if all you want is a shared dictionary to be used as a cache or something, it has to have its own process/goroutine, you don't get an option of just locking access.

Since that's how it works in Erlang, it has a bit of syntax grease around it, but not enough to make it just right; you've still got to do things like handle communication errors because it could be on a different node in a cluster whereas in Go it's just a local shared resource.

I think the main problem is that as useful as actors are as a concept, they're not a great foundational abstraction, which is to say, the base that everything is built on and you can't go below. It works, but then it means you're paying the full actor price for everything. But you don't always want the full actor price for everything, and you don't need to pay it because in practice "lack of actor isolation" is rarely the root cause for any particular problem, because that's too big a thing to be the root cause.

[1]: If that's too glib for you: https://news.ycombinator.com/item?id=34564228

> The rule for mutexes is, never take more than one.

Ideally the actual rule is, never take a mutex while holding another mutex. You can take multiple mutexes simultaneously if the API supports it. (The problem is the API usually doesn't support it unless you implement the mutexes yourself using lower-level primitives, but that requires not actually using mutexes as your primitive.)

( > the technical rule is always take mutexes in the same order.

As you note, this doesn't actually work in practice, since you've given youself the opprotunity to get the order wrong every single time you do it.)

Mutexes aren't OK even if you only use one. They are error prone, you can forget to unlock. And of course, there is a temptation to avoid using it for efficiency reasons, because you "know" this part of the code is safe.

These days I develop servers on the JVM. We almost never think about mutexes or related things, libraries take care of that. I use Scala, and our entire data model is immutable, eliminating most race conditions. I think I had to declare something as volatile once or twice.

> They are error prone, you can forget to unlock

That's not a problem with mutexes but with resource management in some languages. In Rust mutexes use RAII and unlock automatically - you cannot accidentally forget to unlock.

Yes but it is very easy to hold mutexes locked longer than needed because you usually don't think about when stuff is dropped in Rust, you just let it go out of scope.

I have gotten really annoying deadlocks because of this in the past.

Fair point, but in order to make a deadlock, you need a reference back to the object that holds the lock. And back references in Rust are hard to make. Most of the time if you unlock too late, you get a performance problem, not a deadlock.
> They are error prone, you can forget to unlock

That's a language issue though, rather than a mutex one. It's reasonably straightforward to fix that, as some languages (like Nim) do.

> The story of software engineering in the 1990s was a story of overreactions and misdiagnoses.

This is a recurring theme, not one isolated to the 90s.