19 ms·
Why Concurrency Is Hard
- sillysaurusx 6y agoI’ve been studying pg’s Bel: http://paulgraham.com/bel.html http://paulgraham.com/bel.html It has a delightful idea: to ensure the current thread isn’t preempted, set a global “lock” variable to true. The scheduler checks “lock”. If it’s true, the current thread is added to the front of the threads list. Otherwise it’s added to the back. Then the scheduler runs the first thread in the threads list. So by setting “lock” to true, you are essentially instructing the scheduler never to switch threads.
- hedora 6y agoHow does that help if you have multiple cores?
- diehunde 6y agoI think Go adds even more complexity to concurrency by pushing the CSP model. CSP is hard for many problems where classic shared-memory using locks and atomics are well defined (think of classic concurrency programming textbooks).
- klodolph 6y agoGo provides all of that, you have sync.Mutex and sync.Cond, and you are of course free to use them. People do use them in practice, precisely because some code is easier to write that way. Saying that Go "adds even more complexity to concurrency" doesn't scan. It lets you write code using whichever model you prefer--CSP or mutexes. It is definitely true that CSP is hard for many problems... but locks and shared memory is also hard for many problems. There is no silver bullet, so you need both.
- spyspy 6y agoPretty much every time I’ve pulled out a channel in Go I end up rewriting it to just use a mutex, or even easier, write to individual indices in a slice.
- diehunde 6y agoYou forgot to quote the most relevant part of my statement: "...by pushing the CSP model." Of course you can do whatever you want, but Go is a very opinionated language and there's an opinionated community behind it. Which it's fine. But then when you ask "how to do X using locks," the only thing people respond is the mantra of "don't communicate by sharing, share by communicating". Which doesn't help in any way. Then people ask complex questions about using channels and others say "why don't you just use locks?" That's why I think Go adds more instead of alleviating the complexity of writing concurrent programs.
- klodolph 6y ago> You forgot to quote the most relevant part of my statement: "...by pushing the CSP model." I didn't quote that part because it's the most nonsensical part of the comment. You've apparently been frustrated by your interactions with the members of the community and that's a fair complaint, but you haven't translated that complaint into a criticism of the Go language itself. My experience is--CSP is a reasonable default, and "select" is a really nice language feature to have around, and mutexes / condition variables are still available when you need them. Mutexes aren't a good substitute for CSP and CSP isn't a good substitute for mutexes. You should have both available.
- diehunde 6y agoThat's exactly my criticism. You don't need both. You only add confusion by having both. Just stick to one and improve it. Most successful languages don't have native support for two concurrency models and people have built amazing concurrent software with them. Now with Go, you have this extra problem of "should I be using channels or locks?"
- klodolph 6y agoIn my experience, you definitely want to have both options available. Like you said: > CSP is hard for many problems where classic shared-memory using locks and atomics are well defined (think of classic concurrency programming textbooks). This isn't a problem with Go's particular flavor of CSP, this is just a fact--it can be hard to translate simple modules that use locks into modules that use a CSP paradigm. > Now with Go, you have this extra problem of "should I be using channels or locks?" I think you're arguing against yourself here... if it's important to answer the question "should I be using channels or locks?" correctly, then wouldn't you be in a bad situation if the language only provided channels or only provided locks? This is no different from the problem of "should I use recursion or iteration?" or "should I use a tree or a hash table?" Some times, either option is workable, but at other times, one of the options is better than the other. CSP versus locks is the same thing, it's just another decision you have to make when programming.
- chrisseaton 6y agoCSP also encourages writing very racy programs, as the order in which messages arrive in queues can be non-deterministic.
- pkaye 6y agoWhat are the alternatives that are deterministic?
- chrisseaton 6y agoStructured concurrency - fork and join models, or dataflow models, for example. Or clocked models like a physical circuit.
- joubert 6y agoFork-join and dataflow models are parallel computing solutions. Are you suggesting that one should strive to solve concurrency problems with parallel computing approaches?
- valenterry 6y agoNo they are not. You can easily do concurrency with a fork-join approach on just on thread (no parallel execution). fork-join in the browser with e.g. V8 as the runtime is a good example of that.
- aarongolliver 6y agoI wonder how many years of fork() we had before someone finally had a second core to run things on.
- CyberDildonics 6y agoThis is just a nonsense semantic game. Everything gets synchronized or serialized at some point, even if it is hardware that does it.
- initplus 6y agoIn theory I like channels, in practice I find them challenging to program with. The fact that only the sender can safely close a channel is frequently frustrating.
- cle 6y agoI think the Go docs can be more discerning about when to use channels and when to use shared memory. The broad advice of "share by communicating" is too broad, too many people learn Go and think they need to use channels and CSP everywhere, and then quickly realize that they've over-engineered their program. I typically use shared memory by default (mostly mutexes and waitgroups), and generally only use channels when I'm working with task queues, which is where they really shine.
- PaulBGD_ 6y agoI know this is an excerpt, but there are a lot more reasons that concurrency is hard. One of the biggest ones is that your concurrency can look like it works, until it doesn't. Made worse by the fact that real-world concurrency can be hard to test locally.
- mianos 6y agoThe valgrind helgrind tool is a great tool for testing concurrency locally. Without serious tooling testing concurrency is just 'toy' testing.
- phkahler 6y agoI would suggest a change in wording. Concurrency is not hard, its asynchronous updates that are hard. That's why digital logic circuits use a clock, and it's also why mixing logic into event driven user interfaces eventually gets out of hand.
- bob1029 6y ago*asynchronous updates to shared state If everyone is working in parallel, but on independent tasks, there are no issues.
- CyberDildonics 6y agoEverything gets synchronized at some point. There is no sense splitting hairs on labels just because you don't have to do the synchronization yourself. Those independent tasks are going to have to have their output go somewhere.
- melvinroest 6y agoIn some cases, perhaps most cases, this gets hard. In other cases this is still easy. For example, when the ordering of the output doesn't matter. I'm dealing with such a case in a data conversion program. A super simple example for when ordering doesn't matter is when you do parallel addition.
- CyberDildonics 6y agoI'm not sure exactly what you are saying, but not all synchronization is about ordering. Addition is still synchronization since eventually multiple numbers will need to be merged into one output.
- melvinroest 6y agoSure, and that can cause issues potentially. But since addition is commutative [1], you don't have to wait on an exact ordering and synchronize for that. All you have to synchronize for is waiting until all the numbers are added. [1] for HN'ers that don't know the word, it means a + b is the same as b + a.
- anonacct38 6y agoThe author touches on the reason, but I'd phrase it more strongly. Shared mutable data concurrency is hard because it removes ability to locally reason about code. x=1 x++ assert(x==2) can now fail.
- dragontamer 6y agoThe x++ isn't necessary. x = 1; assert(x==1); // May fail in a parallel world
- mcqueenjordan 6y agoIt's my own weird little take, but I'll share it. In my experience, simple concurrency is easy. Confused concurrency is exceptionally hard. I think about this in the same way I think about C++. C++ is quite nice, if your teams limit themselves to a reasonable subset of the language. But it can become an absolute monster of cumbersome complexity, if these guardrails go away. Concurrency feels the same. If you take a certain view on concurrency, which is "do it as simply as possible, don't break certain rules, don't try to be too clever," I find it fairly straightforward to reason about. At the same time, 9 out of 10 of the hardest things I've debugged have always been nasty, awful concurrency bugs. The reason it was so hard? The concurrency was complicated, interwoven, assumption-laden, and absolutely impossible for anyone to reason about. Implicit contracts between calls required entire whiteboards of non-trivial state to understand, and even then, with all that work, it was a needle in a haystack. I guess I would just say: It doesn't have to be hard. It's only hard if you don't respect it from the beginning.
- joubert 6y ago> Confused concurrency. I’m not sure I know what you mean by that. > If you take a certain view on concurrency, which is "do it as simply as possible, don't break certain rules, don't try to be too clever," Is there a list of those rules?
- klodolph 6y ago> Is there a list of those rules? Several different lists. You have to pick a concurrency model. Within that concurrency model, you pick some set of rules to follow which has the guarantees you want.
- chii 6y agoso how do you know which model to use? How do you know what rules guarentee what outcome? How do you compare them against each other? All of those things mentioned in the original reply is vague, and can only come from having experience in the subject matter. That's why concurrency is hard.
- jedberg 6y agoThe best way to make concurrency easier is to avoid it in the first place. This requires you to think about how correct your data really needs to be. For example, a bank balance. Most people would assume that bank balances must always be correct. But this isn't really true. Back in the days when people wrote checks, your bank balance was almost never accurate -- it was on you to keep a local register and know your balance, and if you shared the account with someone else, to reconcile with them frequently. ATMs also use eventual consistency. When you take out $300 from the ATM, it attempts to update the balance, but it will let you take the money out if the bank is unavailable. Because the bank is willing to risk $300 that you aren't overdrawn. In most cases the "eventual" part is nearly instantaneous so you don't notice, but it doesn't actually have to be. So the number one way to make concurrency easy is to avoid it altogether.
- CyberDildonics 6y agoDelayed syncing is not the same as "avoiding concurrency altogether" or having data be 'less correct'. Those aren't even simplifications for relaxing the latency of synchronization, they aren't correct descriptions at all.
- jedberg 6y agoThe point is in a lot of cases where you think you need synchronization you really don't.
- mannykannot 6y agoYet your example is not one of them - with accounts, the sequence of states must converge on a consistent history as they recede into the past. Eventual consistency comes with it own set of counter-intuitives, and is a multiplier of possible states a system could be in. it is generally not a complication we would choose, but something we must cope with to do things that would otherwise not be feasible.
- CyberDildonics 6y agoMy point was that your simplifications were not correct. Also synchronization is going to happen at some point or there wouldn't be any problems to talk about. Synchronization is what it all comes down to.
- anko 6y agoIt's amazing how much easier it is in erlang/elixir. having lightweight isolated processes and no (or little) shared mutable state matters a lot. :)
- elmo2you 6y agoI will have to agree with this. Until I started working with Erlang/Elixir, I would probably have agreed with the OP article instead. Or at least its title, for reading about concurrency problems has become rather obsolete (for me). For years, I certainly made my own share of concurrency horrors, in everything from C and Python to Java and JS. Eventually I (sort of) got it right in each, mostly by building up experience in approaching it the right way. But it always remained a source of easy to make mistakes (and sometimes near-impossible to solve complexities, with larger systems). However, since working with Erlang/Elixir, I sometimes wonder what the F#$& I was doing all that time. Many ideas were certainly a bit challenging to wrap my head around at first, but once they clicked there (kind of) was no way back. This will probably sounds arrogant, and it's okay with me if I get down-voted because of it. But the more I work with Erlang/Elixir (and a few others in the FP field), the more it becomes clear to me how much the languages I used to program in (and their typical challenges), now most of all look like a (rather sick) joke to me. Again, I know this might sound pretentious, if not offensive. But it is sincerely what I feel. In my defense: I'm not some kind of hipster, still wet behind the ears. I've been programming for nearly 3 decades and in well over a dozen different language. I'm one of those kids that were ripping apart assembly code of "protected" games and desktop software, and hacked around on micro-controllers, during the nineties. While Erlang/Elixir may have shortcomings too (although I personally have not found any), it is one of the best things I've encountered for a long, long time. I'm also charmed by ClojureScript, but for totally different reasons. However, I do like it for when I'm forced to build something more complex for browser/front-end. But that's a whole different story, and not much relevant to this concurrency "issue".
- bxk1 6y ago> I will have to agree with this. Until I started working with Erlang/Elixir, I would probably have agreed with the OP article instead. Or at least its title, for reading about concurrency problems has become rather obsolete (for me). That summarizes the problem with most of the articles and discussions on concurrency. People talking past each other because those who don't do much concurrency, especially complex concurrent software, haven't reinvented Erlang yet and don't understand what the fuss is about, and those who do can't explain in relatable terms how the actor model helps them organize concurrent code, preserve local reasoning, abstract, compose it, tolerate faults, etc. and eventually they stop bothering and so poor concurrency articles and comments continue to dominate the infospace.
- dragontamer 6y agoI've written about it elsewhere, but I think that concurrency is easier if you really stick to a methodology. Fork-join is the simplest methodology, and supported in a very wide number of languages. In fact, its so stupidly easy, you probably are going to over-think it the first time you use it. 1. Fork when you need concurrency (aka: speed) 2. Join when you need consistency That's it. Don't write mutexes, don't write barriers. Just join. That's all you need: if you ever need "synchronized" access, do it in the "single" or "master" thread, when all other threads are joined. You _cannot_ have a race-condition if you only have one thread. Joins ensure that only one thread is running. What else do you want? Fork-join parallelism has relatively low utilization. Its not the fastest approach, but its the "easiest" approach in my eyes. ------------ The #1 issue with parallelism books, in my eyes, is that we start with mutexes and threads, which are a very hard way to do parallelism. I argue instead: beginners should start with easy (but inefficient) forms of parallelism, and then work their way towards complicated tools as performance becomes more-and-more of a consideration.
- DrBazza 6y agoThis reminds me of this thread: https://news.ycombinator.com/item?id=21919988 https://news.ycombinator.com/item?id=21919988 and then there was a follow up to it saying the measurements were wrong IIRC, and then a further article showing, more or less that getting your own mutex replacement correct is so tricky that for 99.99% of programmers, just don't bother.
- scott_meyer 6y agoConcurrency is hard because we invented a bunch of theoretical machinery (semaphores, mutexes, condition variables,...) and embedded it in languages such that most of the programs one can express are bad: racey or deadlock prone. Compare with modern sequential languages which have near-perfect denotational semantics: anything which compiles means something. If you want practical guidance, my best is to avoid all of the concurrency constructs that are taught in schools. Use log-structured single-writer multiple reader, wait-free, processes and shared memory.
- beders 6y agoA simple way to avoid the problems highlighted in the article is to use immutable data-structures. If you need to update that across threads, am atom with CAS logic will usually be sufficient. If you like it erlang style: an agent can serialize changes nicely. If you need coordinated changes, use software transactions. Of course: next we'll introduce deadlocks with DB transactions. Fun!
- galaxyLogic 6y agoWhy concurrency is difficult? Because we cannot have multiple parallel thoughts
- amelius 6y agoWomen can. https://www.bbc.com/news/science-environment-24645100 https://www.bbc.com/news/science-environment-24645100
- galaxyLogic 6y agoGood point. But as we know multi-tasking is really not truly parallelism, it just means interleaving and keeping the state of multiple sequential lines of thought in our "head-stack". Async is difficult I believe because "thinking" in inherently sequential because it is based on mentally reciting some (serial linear single-dimensional) language in our head. An alternative could be some visual language where parallel and even 3D lines can be drawn. A mental image of it can be in our head. So I think async inherently needs some kind of visual language to make it easier to grok.
- dehrmann 6y agoFor all running threads, imagine that every operation can interleave in any possible way. Since the permutations grows in factorial time, the only way to manage concurrency is through well-defined interaction points with no more than a few interesting parts of execution. Are there only 4 possible sequences of significant instructions? Cool. Almost any more and it gets hard to reason about because there are so many paths that aren't obviously enumerable.
- mrbald 6y agoProblem of getting the concurrency right is very much identical to the problem of getting the system design right. In both cases it takes a lot of hard work to learn how to not produce "dead-borns" or "genetically pre-disposed". In both cases the course of "least resistance" (using intuition instead of logical reasoning) used by many as the default will most likely fail you. To develop an intuition of what is easy and what is hard with both system design and concurrency design is a matter of years of hard work of trial and error. The actual question is why the complexity is so counterintuitive. I think this is to do with the Dunning-Kruger effect (the less you know the more confident you are). In both cases the solution seems to be to keep beginners away from this and to have the same consistent, peer reviewed, approach across the system, and a process of improving it in a structured way. And of cause prefer higher level abstractions -> computation graph, pipeline, event loop, etc. over just hacking an ad-hoc Frankenstein out of low level primitives (which seem totally "simpler" if you ask a beginner).
- Traubenfuchs 6y agoModern, sane and productive languages usually come with strong abstractions for concurrent usage, e.g. atomic reference wrappers, thread safe wrappers (AtomicInteger) or special methods (Interlocked.increment) for numbers and concurrent collections. With them and a basic understanding of single system concurrency, single system concurrency is and always will be trivial.
- mrbald 6y agoThey are low level abstractions and they don't save from the hardest to debug and fix problems - race conditions and inconsistent states, especially in distributed applications. No mainstream languages give you a solid high level abstraction to build on. Some frameworks, kind of, do: ASIO in C++, AKKA Typed Actors and Reactive Streams and JDK CompletableFuture in JVM world, etc.