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 ?
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.
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.)
Multiple candidate solutions have been proposed:
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).
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.