Why are multi-threading programs more prone to errors than single threaded programs ? or why is multi-threading programming one of the areas where developers are (more) prone to errors? Is this in general or is it valid only for scenarios where there are shared resources being used by multiple threads ? Are the difficulties to detect errors or simulate bugs in multi-threaded programs just related to inexperience or immature tool support, or are they fundamentally more difficult than the same tasks for single threaded programs ?
-
1$\begingroup$ 1. More prone to errors than that? What are you comparing to? 2. This is too broad, because you ask two unrelated questions: why are they more prone to errors, and what tools can you use. One question per question, please. It'd help if you could edit the question to clarify these points. $\endgroup$– D.W. ♦Dec 2, 2015 at 7:50
-
$\begingroup$ Also, the core question does not seem to be a computer science one, but one for experienced programmers. Community votes, please! $\endgroup$– Raphael ♦Dec 2, 2015 at 8:45
-
4$\begingroup$ Hints: 1) People tend to resp. are trained to think sequentially. 2) Non-determinism enters the picture. 3) The space of potential states the program/system can be in explodes exponentially in the number of concurrent actors. $\endgroup$– Raphael ♦Dec 2, 2015 at 8:46
2 Answers
Overview
Multi-threaded programs are more prone to errors than single-threaded programs because of the problem of concurrency bugs. Concurrency is hard for most developers to reason about, which causes many bugs in multi-threaded programs; this issue normally doesn't arise in single-threaded programs, because single-threaded programs typically have no concurrency.
Mutable shared state, and plan interference
The core problem is due to the combination of multi-threaded programming plus mutable shared state. If there was no shared state, or if all shared state was immutable, then you wouldn't have to worry about concurrency bugs. But when you have mutable shared state, then the programmer has to start thinking about all possible interleavings of the thread A's accesses to the shared state with thread B's accesses to the shared state. There can be exponentially many possible interleavings that the programmer needs to reason about, if they want to avoid concurrency bugs, and no surprise, that's non-trivial.
Mutable shared state introduces the possibility of data races and other kinds of concurrency bugs. These can be tricky to reason about.
Multi-threaded programming makes it easy to have plan interference problems. Programmers tend to think in a single-threaded way: they think about a single sequential path of execution. When they're writing the code for a particular function/method/module, they build in their head a "plan" of the operations/actions they want to have happen, typically based on a single-threaded mental model (e.g., based on the implicit assumption that nothing else will interrupt them in the middle of this sequence of actions or concurrently modify their internal state). However, when you have multiple threads with mutable shared state, they can cause unexpected interference while the code is in the middle of execution (changing something out from under the code while it is in the process of executing the programmer's plan).
For example, here is one classic example of plan interference. Imagine you have a publish-subscribe data structure that lets clients register themselves as observers, and delivers events to all registered observers. Imagine its code is in the middle of delivering an event to the observers, one by one, and the code of one of the observers happens to mutate the underlying state; this mutation might cause the notifications to the other observers to happen in the wrong order, or to expose an inconsistent state to them.
Further reading
For a more general description of the plan interference problem and more general discussion of why multi-threaded programming is more error-prone, I recommend the following papers:
The Problem with Threads. Edward A. Lee. IEEE Computer, May 2006.
Concurrency Among Strangers: Programming in E as Plan Coordination. Mark S. Miller, E. Dean Tribble, and Jonathan Shapiro. TGC 2005. (See especially Sections 1-4.)
Candidate solutions
Multiple candidate solutions have been proposed:
One possible solution is to avoid using threads, and program in a sequential single-threaded way. This is how Javascript works in browsers, for instance.
Another is to use threads but avoid all mutable shared state. For example, Erlang.
A third is to use message-passing rather than mutable shared state for interaction between the threads. One can build systems based on the actor model. For example, E.
A fourth is to use threads with mutable shared state, but just be really careful. You can have systematic ways of locking shared data and preventing data races and avoiding plan interference and so on.
See also https://cs.stackexchange.com/a/27803/755 for similar ruminations (I borrowed some text from there).
-
$\begingroup$ "Concurrency is hard to reason about" -- is it, really? I mean, almost nothing is computationally tractable, but in principle it is easy, isn't it? $\endgroup$– Raphael ♦Dec 2, 2015 at 17:51
-
1$\begingroup$ @Raphael, Agreed. My statement was intended as an empirical/anecdotal statement about human tendencies rather than about what is possible in principle. It's not intended as some kind of statement about decidability/computability/etc. (and virtually anything you'd want to reason about, is undecidable anyway). I freely confess that I don't know how to quantify that claim about developers, and I don't have any scientific evidence for it; rather, it's my sense based upon experience and anecdotes. Surely someone must have studied it, but I'm not familiar with the literature. $\endgroup$– D.W. ♦Dec 2, 2015 at 18:45
there is some long scientific study on this subject. there are different ways to answer this but one standout aspect of multithreaded programming is how much it has evolved over many decades. decades ago, it was less understood and even more of a research topic, and there were not standardized ways of accomplishing multithreading. in multithreading, there are many ways to do nearly the same thing and some ways are more complex or error-prone than others. it has taken a long time and major research ingenuity to identify/ sort out these patterns.
early computer systems were not multithreaded and even major commercial operating systems such as windows OS/ Macintosh a few decades ago (early 1990s) had unsophisticated/ more primitive multithreading approaches such as nonpreemptive multitasking where a single "misbehaving" program could interfere with other programs.
multithreaded programming predates design patterns study. design patterns have had a major influence on multithreaded programming theory.
so the idea that multithreading programming is inherently bug prone has somewhat lessened over time. it is to some degree inherently tricky but if the developer is trained well in theory/ standardized design patterns and uses them instead of avoiding them, the bug prone aspects of multithreading can be significantly mitigated. (this could even be seen as one of the major "success stories" in computer science in general.)
as R points out in comments, multithreading is inherently typically nondeterministic and not "repeatable" unless specifically designed that way. the exact same code may run in different thread orders depending on very subtle factors such as current CPU load etc. and this can complicate troubleshooting.
a major design pattern now widely available in many languages (not always under the same name) is known as Communicating Sequential Processes where thread safety is provable/ verifiable and not hard to guarantee if the patterns are followed. it has strong similarities to the unix pipeline concept.
see eg
-
$\begingroup$ nice standardization/ patterns in the area can be credited to computer scientist multithreading specialist Doug Lea esp wrt Java. another key point alluded above is that thread bugs are often intermittent and probabilistic and not easily repeatable. intermittent-manifesting bugs can be very difficult to isolate. or the "same underlying problem/ bug" may occur somewhat randomly in different threads etc. $\endgroup$– vznDec 2, 2015 at 18:24