8 ms·
Why Johnny Can’t Write Multithreaded Programs
- jaimebuelta 13y agoTo me, saying: "Multithreaded programs are not difficult, you just have to know how to do them" is extremely similar to "Multithreaded programs are difficult, and you have to know how to do them" I've done a lot of multithreaded programs, and, yep, you have to use some good practices to avoid making the code explode. You know, having to learn them and stuff is why they are "difficult". Just because you know how to get something done does not make that "easy". That's what experience is for.
- kunil 13y agoI am in no position to call my self I can write good multi threaded programs but... damn people are bad. Once I worked in an embedded project that had sleeps in UI thread and then people started to use threads for 'optimizing'. It didn't go well
- notacoward 13y agoHis argument for "if you use a lock you did something wrong" seems to presuppose that somebody else created handy concurrent data structures for you. In other words, he admits that multithreaded programming is hard, so you should make it Somebody Else's Problem. I don't think that's a very strong argument.
- rdtsc 13y agoExactly. Unless the language/framework/library has automatic serialization and copying of objects, or automatic pointer ownership transfer when put in these magic queues, the problem is still there.
- lmm 13y agoStructured programming is not hard - you just use someone else's implementation of loops and functions and classes. If everyone tried to build those themselves from low-level primitives, structured programming would be seen as just as hard as multithreaded programming currently is.
- notacoward 13y agoFalse analogy. For one thing, structured programming covers control flow and not just data structures. For another, things like for loops and functions have been in every language for decades. Processors are even designed to support them. Concurrent data structures are nowhere near that level of maturity or ubiquity. Many people still work in languages and environments where they can't reasonably be assumed. Even if the OP's argument is valid within some specific context, it's still being oversold as general advice.
- Nursie 13y agoHalf the world seems to operate on the principle that this sort of stuff is SEP these days, and then sneer at the people who are solving it for being so low-level and not at all modern...
- sophacles 13y agoA different take on this: we have a small team. We are solving in a hard problem. Other people have solved this other hard problem for us. Let's focus on doing our thing very well, and not spend our limited cycles on redundancy. I like to drive the point home with this (slightly hyperbolized) statement: X already does this, and those folks need it to be good if they want to eat, so why the heck are we doing it ourselves?
- Nursie 13y agoAgreed, code re-use is a very valuable and great thing. Where there are well-tested and well-proven technologies to do what you're trying to do, use them (if the license fits, it's not too expensive etc etc.) But someone has to do the 'hard' parts, and do them well, and I don't see why competent tech folks shouldn't at least be interested in that.
- anaphor 13y agoExactly. Things like this just deter people from becoming interested in "low level" things like compilers, runtime systems, databases, etc... and maybe becoming active researchers in these areas.
- deleted 13y ago[deleted]
- deleted 13y ago[deleted]
- sz4kerto 13y ago> If you need a lock, then you probably have global mutable state that has to be protected against concurrent updates. The existence of global mutable state indicates a flaw in the application’s design, which you should review and change. No, no, no. If you are writing multithreaded programs, you _will_ have locks, period. The question is whether you explicitly use them or if they are hidden somewhere in your libraries, frameworks, etc. Actually, the latter are much more difficult to debug. Most of us have probably seen situations where deadlocks or data corruption has occured because of some extremely complicated interaction of message pumps - UI thread - callbacks - async operations - etc., without any explicit locking. > Perhaps most importantly, modern programming languages and libraries make it easy to create a producer-consumer application. Yes, that's great again, until the deadlock-corruption-indeterminism problem comes back again in a different abstraction level. There is _no way out_. If you are writing concurrent apps, you have understand what's happening. By concurrent I mean concurrent on the level you are working on. Using a parallel 'map' operation is different because the concurrency is an implementational detail from your perspective. But if you are writing a massively concurrent quoting/trading engine, then you have to know what's happening. And if you do, then you're free to use locks, executors, async, whatever -- it's just a tool to solve the problem. ... The more I look at this article the more ridiculous it seems...
- temujin 13y ago"If you are writing multithreaded programs, you _will_ have locks, period." Not necessarily. In scientific computing, it is common to encounter "embarrassingly parallel" scenarios where each thread can solve equal-sized chunks of the problem, and you can wait till all threads have terminated to collect results. edit: Okay, the final wait is usually implemented with a lock under the hood. Nevertheless, this type of program is straightforward to reason about.
- sz4kerto 13y agoAnd of course that 'wait' operation does not involve locks and/or condvars. Oh wait.
- 13y ago
- timje1 13y agoPersonally I've found that learning a functional programming language, with strictly enforced 'no side effects' functions, has simplified multi-threaded programming for me. I just remember back to those restrictions and force myself to follow them, whatever the current language allows. For reference, the language was Erlang, which I believe has the best handling of threading that I've encountered so far.
- tel 13y agoAgreed! My mind on concurrent programming is far away from the usual nightmare stuff because I just think at the level of immutability, MVars, OTP, STM, LVish. You have to simplify your computational semantic domain before it's easy to augment it with nice abstractions for concurrency.
- zvrba 13y agoSo you've basically experienced the benefits of actors (message-passing style, no shared state). But you can use message-passing paradigm in imperative languages too, though it's rather cumbersome in non-extensible languages like C or Java.
- jerf 13y agoI think (s)he strongly implied that. I'd simply flat-out say it, the best way to learn how to program in an imperative multi-threaded environment is to clock enough time in Erlang, Haskell, or perhaps Clojure (with as much immutability as possible) that you can get real work done (not merely finish a couple of tutorials) that you are forced to learn how to program in a minimal-sharing style, then if you go back to something like Go or something, it's really not that hard. I think you can learn how to program multithreaded code well without that, but you'll learn a great deal faster if you use a language that kicks you in the face every time you do something stupid (or simply doesn't let you do it). As in, potentially an order of magnitude faster. Fast feedback is non-linearly better than slow feedback. Though this does assume you get to greenfield the multithreaded code. Woe betide ye who have to retrofit existing poor multithreaded code. I don't even know what to tell you, except you have my pity.
- henrik_w 13y agoOn the subject of multithreading, I like this one by Ned Batchelder[1]: Some people, when confronted with a problem, think, "I know, I'll use threads," and then two they hav erpoblesms. [1]: http://nedbatchelder.com/blog/201204/two_problems.html http://nedbatchelder.com/blog/201204/two_problems.html
- colinshark 13y agoMultithreaded code is more difficult to write and more complex to debug by any objective measure. "There are difficult multithreading scenarios, just as there are difficult scenarios in the single-threaded world. But those are relatively rare." Is this some sort of flame bait?
- rdtsc 13y ago> If a multithreaded program is unreliable it’s most likely due to the same reasons that single-threaded programs fail: The programmer didn’t follow basic, well known development practices. Well no. It is like saying mt programs are broken because the user didn't know how to write them. It is not saying much. Everything with bugs is broken because the writer didn't know how to write it. The reality is that multi-threaded is hard because it is hard to reason about and hard to get right. In one large application it only takes on junior programmer to dip their fingers in and add that one module that will crash the system 1 out 1000 run on customer's hardware only. MT issues and problems on a whole new level of difficulty that is qualitatively different than what single threaded programs experience. Sequential problems and bugs are still there, but now there are a host of completely new things happening. > These programs very often fit nicely into a producer-consumer model with three threads: * The input thread reads data and places it on the input queue. Well, you also have to make sure the queue is thread safe. Understand what does it mean to get an item off of an empty queue. Now you have queue-using-up-all-my-memory problem if you do stream processing with mis-matching processing rates between parts of your programs. What about objects you put in the queue. If those have any pointers or are pointers themselves now you are back to the original problem. So make sure objects are copies, are serialized/deserialized or there is proper ownership handoff. In summary, is it possible to write correct highly concurrent programs in a shared memory model with threads/and-or/events -- yes. Is it easy? No, it is not easy. The article correctly points the often common fallacy that using event/callback-based single threaded approach is superior and avoids all the concurrency related issue. It doesn't avoid all issues. Now the race is between callback/errback chains instead of threads. Granularity is much higher (if incrementing a shared 64 bit int is fine without a lock). But the need to protect shared data structures is still there. The danger is psychological because one thinks "hey this is safe, I don't need anything to worry about" That is why I believe it is better to use a language/system/framework that uses isolated heaps and rigidly enforces separation between concurrency units. Erlang has that, D has some of that, Nimrod, Web Workers and Dart isolates to that. You incur a speed penalty for that. Don't be shy about using regular Unix OS processes. Those are great for isolation and do it well. The cost of IPC and cost of creating them can also be high. You in incur a penalty for copying data between concurrency units, but remember, there is a bigger penalty you incur when the program has crashed. The converse of that is, the biggest performance gain your application will achieve is when it goes from not working to working and staying working.
- JeffJenkins 13y agoIs there a list of languages which use separate heaps for each thread? I'm really interested in learning more about that concurrency model, but haven't had much luck compiling a comprehensive list of which languages actually use it
- bwindels 13y agojavascript with webworkers
- stevoski 13y agoWriting a background thread that does a long-running calculation? easy. Writing a background thread that can be cancelled immediately, that gives useful progress information, that doesn't leave other threads indefinitely blocked if cancelled, and that cleans up after itself if cancelled? hard. For my single-threaded code I have great static analysis tools, IDE magic, and testing frameworks. For my multi-threaded code, I'm on my own...
- Pxtl 13y ago> For my single-threaded code I have great static analysis tools, IDE magic, and testing frameworks. For my multi-threaded code, I'm on my own... That's a big point. The tooling for parallel code is developing really slowly compared to the proliferation of multi-core processors. Most of it seems to be "we'll make the library multi-threaded but all your code will still be single-threaded" like we see in Node.
- exDM69 13y agoI have written some multithreaded code and I can agree that it's not too difficult to write. What is difficult is testing and debugging multithreaded code. There are lots of corner cases that you might overlook when writing multithreaded code, and some of them will only ever happen if the threads get scheduled in a particular manner. Multithreaded software development is not easy, partially because in the past we've acquired practices that don't work very well in multithreaded world. Partially because it's a whole new world which is not thoroughly explored and established best practices do not exist. But all in all, too many people are afraid of multithreaded code and this is holding us back. It's not scary or difficult, it just requires a more rigorous approach at times.
- throwaway0094 13y agoWhoa whoa whoa. Slow down. When did I become an Applications developer and not a systems developer? The kernel I work on uses TONS of threads and locks. And sure, it's fucking crazy to debug. But I don't think you'll be prying mutable state from the cold dead hands of kernel developers any time soon.
- the_af 13y agoI think it's pretty clear if you're a systems developer (or someone writing a library with concurrent data structures), this article doesn't apply to you. The majority of developers aren't going to be systems developers, though. Just as the majority of developers don't use assembly language either.
- mannykannot 13y agoExactly. Someone has to deal with concurrency so that, in many cases, applications programmers don't have to.
- fatman 13y agoThis is one of my largest weaknesses. I haven't come across any good resources for self-study on this topic. I haven't found any online course-type resources, and the books i've found are hard to follow. Any recommendations for how to learn this stuff would be very welcome.
- camperman 13y agoWould people be interested in a getting started type tutorial using Zero MQ? As I replied to thewarrior below, the manual is very good and it made multithreading an absolute pleasure for me but it's not really an introduction to threads - I had to experiment quite a bit. I can do it for C, Python and Lua. What would be useful examples? Multithreaded video decoding? Scaling out calculations to multiple cores?
- fatman 13y agoThat would be awesome. I just got on a new project a week ago where the PM sent me a link to Zero MQ and basically said "here, read this".
- kasey_junk 13y agoA bigger problem than people writing concurrency poorly is that they jump to concurrency too soon and think it is going save them from their performance problems. It almost certainly won't. Contrary to the growing popularity of message passing concurrency, modern commodity hardware is not massively parallel and jamming massively parallel algorithms on top of it generally is a mismatch. You are almost certainly better off having a few well defined threads that are fast than unbounded threads that are slow.
- chrismorgan 13y agoI like Rust's approach (it's not new, but Rust does a good job of the total implementation). When I'm using Rust, I know that data race in safe code is impossible. (I know this isn't quite all there is to the problems of multithreading, but it's a very high percentage indeed in almost all applications.)
- gnuvince 13y ago> The programmer didn’t follow basic, well known development practices. Multithreaded programs seem harder or more complex to write because two or more concurrent threads working incorrectly make a much bigger mess a whole lot faster than a single thread can. Actually, I prefer to think that the reason multi-threaded programs are harder to write is because it's harder to compose components together. If I have a regular old function and another regular old function and compose them, I get a regular old function and I know that it works how I expect it. With two multi-threaded functions, then I have no guarantees anymore. If I cannot take simple components and combine them into more complex ones, how am I suppose to write correct code? Sorry, but I'm just not good enough.
- Jtsummers 13y agoThis is where language choice and concurrency libraries become critical. Erlang's (IME) processes are nearly as composable as functions. With golang's channels allow something similar. ZMQ seems to provide a similar mechanism in a language agnostic fashion, though that seems to be about connecting multiple processes rather than threads (do people use ZMQ for interthread communication and syncronization?).
- joosters 13y agoIMO the problem with threads is the 'all-or-nothing' approach to shared data. If you want to make use of multiple CPUs, you either use threads, in which case all of your process data is shared, or you use multiple processes, where none of the data is shared. It is the over-sharing that trips people up with threads. Ideally, we need an API that allows selective data sharing, so you greatly reduce the chance of one thread trampling over bits of another. Right now, the common way to do that is with multiple processes and shared memory regions, but it is cumbersome to set up and requires the programmer to treat the shared data very differently (e.g. one process can't just malloc data for a string, say, and then let the other process access it. The other route is through message-passing, but again you have to write your code in a totally different way. Someone please invent multi-threading with selective sharing!
- wehadfun 13y agoThanks for the article. When I have free time I will look into the various tools that modern languages provide to solve problems that are usually handled with threads. I would have appreciated an article on the various "libaries" we should be using instead.
- VLM 13y ago"Perhaps you’ve accepted the common fallacy that “Multithreading is hard.” It’s not." Bzzt full stop epic fail. Think about the other discussion today about the "naturalness" of zero or one based arrays. If multithreading was natural and inherent part of all noobs, you'd teach programming beginning with multithreading and maybe later on you'd have some weird module which confused most of the class explaining how some legacy languages and systems only allow one thread which has certain unusual programming constraints that most of the class would screw up. Also all automata theory classes would begin with the eNFA as the base class and later on tell the kids about DFAs being broken forms of a NFA and all that. I'm just not seeing it. The article did fail at architectural level. Most non-threaded programs can't be upgraded to threading because either its not worth the effort or its impossible to solve the actual problem without threads so the postulated single threaded solution can't exist therefore you can't upgrade what could never have been implemented. Why are you multi-threading? Most likely if you're adding multi-threading explicitly to a single threaded app for an inherently multi-threaded problem, you're going to spend all your time figuring out how the single threaded part exploited some other component of the entire system to create a psuedo-multi-threaded app. Perhaps it relies on the OS to spawn multiple clients in a client server arch where all the processes live on the same CPU or something. The problem isn't just adding a thread so you can checkmark a marketing box that its now a multi-threaded app so someone can collect an end of year bonus, the problem is you need to scrap the whole thing and start architecture work from first principles. Another funny part was the natural assumption that all programs/projects will be huge enough to contain at least two completely non-interacting subsystems so no global state is the ideal. That's a dumb architecture design or business decision. Lets think of an example where you would have threads with no global mutable state. How about a tetris implementation in one thread and an enterprise rational DBMS system in another thread and an IRC client in another thread, all in the same program/project. That's just a dumb architectural idea, make totally separate projects. Make the smallest usable debuggable tools and use them together, not the largest undebuggable swiss army knife the world has ever seen.
- thewarrior 13y agoAs a n00b programmer I must say that the author dosen't give any examples on how locks are commonly misused or exactly how to avoid these pitfalls other than to say "Avoid global mutable state".
- camperman 13y agoI am not a n00b programmer by any means but I've never needed to use threads until recently. Non-trivial threading - like decoding video in the background and coordinating with audio kind of non-trivial - is hard. Difficult to reason about, unbelievably frustrating to debug, errors seem random, races, deadlocks - you name it, I struggled with it. That all went away when I discovered Zero MQ (http://zeromq.org/intro:read-the-manual http://zeromq.org/intro:read-the-manual). The manual claims: "If you've spent years learning tricks to make your MT code work at all, let alone rapidly, with locks and semaphores and critical sections, you will be disgusted when you realize it was all for nothing." It's not exaggerating. ZMQ's multithreading is marvelous. No mutexes, no locks, no semaphores, no critical sections, no load problems - just perfectly scalable threads that work with any language on any platform. In fact it's so cool, that if you don't have a problem that needs multithreading, go out and find one.
- kasey_junk 13y agoI've used Zero MQ a lot and really like it. It can make writing concurrency applications much simpler. That said, it is important to note that the library itself is using synchronization primitives. Understanding when and how ZMQ does this is very important for optimizing throughput.
- camperman 13y agoYeah, but as an introduction to multithreading it's awesome. You will still learn how to create threads, how to communicate with them, how to synchronise them and how to avoid sharing state if you use Zero MQ. None of that is abstracted. But once you've got the principles down, then you can dig deeper or do it by hand with pthreads or your OS's library if you feel the need. The main pain with learning threading is you have to wrap your head around the principles of it - which are fairly foreign - while at the same time battling with the very strange and frustrating errors because it's so easy to get wrong. ZMQ teaches you the principles without the errors after which you can go master the low level bits if you wish.
- jebblue 13y ago>> If you eliminate shared state, you have no reason to misuse those synchronization primitives. A multi-threaded program that doesn't need to share state probably isn't the usual case, not from my experience anyway. Locks are not evil, think of a railway station, there are sections of track that must be shared by multiple trains.
- mannykannot 13y agoThe author seems to think it is all about avoiding mutable global state, but it is more than that: it is about shared resources and temporal ordering, and these are not issues that can, in general, be avoided in the simple way he supposes.
- jheriko 13y agoits interesting - but there is something fallacious here: "Probably the most important lesson to be learned from the past 60 years of software development is that global mutable state is bad. Really bad." This represents a misunderstanding about the badness of global variables and the goodness of encapsulation in my mind. Mutable global state is an unavoidable necessity to ship your app - every single pixel on your screen is a bit of global mutable state. I won't dive to far in here, but I cringe everytime I see these fancy pants words applied because its pretentious /and/ trivially measurably wrong (see everything on your monitor right now).
- unclebucknasty 13y agoI think there's another simpler factor: some people are just naturally better wired to think in terms of concurrency. There's a certain thinking style you bring to writing and understanding concurrent software. You need the ability to "visualize" the contexts, behavior, and data access. You need to see the possible race conditions and understand what resources need to be "protected" and how. Reminds me of recursion. Back in my CS days, it just made some people's heads explode. They couldn't think in a nested fashion to understand what was going on at various recursion levels, nor what was required to terminate, what should be returned, etc. Like-wise with proofs in geometry. Some people just couldn't get there. But, for those who could, it came without too much effort. You kind of either have it or you don't. You can practice and improve, but some people just naturally "get it". So, I don't think it's as simple as "they learned the wrong approach". In fact, if they truly understood the concepts and possessed the requisite thinking style, they would be able to identify the "wrong approach", and the right one.
- yonran 13y agoI think the article has some good points, but he should spend more time discussing what circumstances you can block when waiting for a result, and how to balance too much blocking against too many asynchronous states. I think his focus on “global mutable state” is a bit of a red herring. It makes no difference whether the mutable state is global (protected by a lock) or owned by a single thread. You can refactor all your synchronized {...} blocks and replace them with executor.execute(futuretask); futuretask.get();, where executor is a single-threaded event loop and futuretask.get() blocks for the operation to complete. This refactoring would make a single thread the owner of the state, but there would be exactly the same number of deadlocks. Conceptually, you can think of your critical sections as a separate thread on which you invoke methods and then block until completion. After refactoring to use Executor and BlockingQueue, one still has to make sure there are no cycles in which threads are allowed to block waiting for which other threads.