5 ms·
I think it's gotten easier over time, actually, and even when this was written it was theoretically out of date. There is a famous paper about how bad threads
by jerf 4y ago
I think it's gotten easier over time, actually, and even when this was written it was theoretically out of date.
There is a famous paper about how bad threads are from the 1990s. I believe it is valid on its own terms but misdiagnoses the underlying issue. The underlying issue is this: Taking multiple locks at once is not a sane operation at scale. Any system built on this will fail.
There is no magic bullet that solves all concurrency problems, but a combination of techniques, used with discipline, take it from insanely impossible to merely challenging:
1. Assign data to a thread, with only that thread able to access it. Offer concurrency via message passing to that thread alone, and don't allow references to escape from the thread (pointers, whatever you want to call it). Language support for this is nice (Erlang and Pony do it one way, Haskell another, Rust yet a third) but not technically necessary.
2. Using locks is valid, and even quite helpful and useful, but never let yourself take more than one. If you need transactions, see #1. Don't write code that encourages people to take more than one. Don't forget about things that may take locks without you thinking about it (e.g., in Go, taking a mutex and then trying to send on a channel is taking two locks); this is one thing that can be a bit of a challenge.
3. For more complicated things, use libraries as much as possible that have solved the super complicated cases already. For instance, databases that offer transactional security. You can do this even if you don't otherwise "need" such databases. Or libraries that offer a "parallel map" within your language, or other canned solutions to parallelism/concurrency.
4. Use whatever support your language provides for "linting" your conditions. The Go race condition checker is theoretically insufficient but practically useful (passing it doesn't mean that you have a safe program but it's a good start). Your language's mileage may vary but take advantage of what you can.
Honestly, just these things taken together can take you quite far. Again, these aren't magical solutions. There is an irreducible sense in which threading is harder than not threading, and I don't expect that to ever change. There will always be programmers that are more expert in it than others. But it doesn't have to be an impossible challenge anymore. And to a large degree, the challenge becomes less "solving the multithread problem" and more just figuring out how to architect systems under the restrictions of the patterns that work and compose together.
Although all that said, at a browser scale concurrency is always going to be a problem. They gotta go fast, but that fights the safe patterns pretty hard because they do involve a certain amount of runtime slowdown (e.g., passing a message is always going to be slower than just not passing a message). Rust is the only practical solution at that scale. But just as You Are Not Google, You Are Not Writing A Browser. More normal programs can go a long way with just what I laid out above.
- secondcoming 4y agoWhat is ‘browser scale’? I’d imagine most browsers spend most of their time idle.
- Slartie 4y agoIf you want to see some of the most complex multithreading systems, take a look inside modern browsers. That's what he means by "browser scale". I have some knowledge into working with Chromium's internals, and it is crazy what's going on inside there. Several different job and message queue systems interlocking with each other, tasks spread across multiple processes getting split and regrouped and synced and recombined, practically everything running async, just so a few pixels are rendered a little faster when your multicore machine updates a small section of a website. Browsers are indeed idling a lot, but if you want them to do something, they must be as crazy fast as possible. The competition in that space is ridiculous.
- jerf 4y agoIn the tens of millions to hundreds of millions of lines of code. Browsers are some of the largest things out there. See also desktop office suites, multi-decade CAD programs, a few other things. Few programs are at that scale. IIRC, even the Linux kernel isn't really at that scale, once you clean away a few files in the drivers that are just literally millions of #defines or something. If you're writing at that scale, by all means copy the necessary practices, but don't just casually assume your need is super large and you've just gotta use the hard-core lock-based concurrency because it's the only option for you. You can get a long way with much nicer architecture that is only slightly slower. For instance, I write a lot of Go. I've sometimes gotten a bit nervous about writing a goroutine (/thread) that owns some particular data structure because I'm like "wow, a lot of things are going to access this, is this going to become a bottleneck in the system?" But every time I've run the analysis carefully, the answer generally comes back not just "no" but "heck no, not even close". Your intuition can deceive you very easily. Locking a single structure behind the limit that "we can only dedicate at most 1 CPU full time to maintaining this structure" is, in practice, far less scary than it sounds. 1 CPU is a lot of power and you generally need a lot of other CPUs trying to access it simultaneously to overwhelm it. Ahdahl's Law kinda works in your favor on this matter (for once); for something like this to be the bottleneck, it needs to approach accounting for (1/(CPUs - 1)) of your execution time, and for that to be the case, those other processes hammering this 1-CPU-locked data structure need to be doing whatever else they are doing very quickly. As with anything in discussions like this, it absolutely can happen! But always check your intuition concretely before going crazy adopting something more complicated and "higher performance"; odds are good this isn't a problem for a given "random" data structure.