Concurrency models are the conceptual frameworks that programming languages and systems use to manage multiple computations that overlap in time. The central problem they address is simple to state and notoriously hard to solve: when several activities read and write shared data, or coordinate their progress, how can the programmer write code that is correct, efficient, and reasonably easy to reason about? The difficulty is not merely that computers have multiple processors. Even on a single processor, an operating system can interleave the steps of many threads, and a program's behavior can depend on the exact order of those interleavings. Concurrency models are the different answers to the question of how to structure a program so that this interleaving is controlled.
The field is not a single, settled theory but a landscape of rival and complementary approaches, each with its own assumptions about what the programmer should manage explicitly and what the system should guarantee. The major approaches can be grouped into a few broad traditions: the shared-memory, lock-based model that dominated for decades; the actor model and other message-passing styles; software transactional memory; and a family of more recent "structured" and "dataflow" approaches that try to make concurrency more compositional. These are not a simple linear succession. They coexist, influence one another, and are often combined in modern systems. The history of the field is largely the history of attempts to find a model that is both expressive enough for real applications and simple enough for programmers to use correctly.
The oldest and still most widespread model is the shared-memory model, in which multiple threads of execution run in the same address space and communicate by reading and writing common variables. This is the model of the classic POSIX threads (pthreads) library in C, of Java's Thread class, and of most operating-system-level threads. The model is powerful because it is close to the hardware: a thread is just a sequence of instructions, and the shared memory is just the machine's RAM. But this power comes at a price. Because the threads can interleave at any instruction boundary, the programmer must ensure that operations on shared data are atomic—that is, that they appear to happen as a single, indivisible step. The standard tool for this is the lock (or mutex): a thread acquires a lock before entering a critical section, and any other thread that wants the same lock must wait until it is released.
The lock-based model has a well-known set of problems. Deadlock occurs when two threads each hold a lock the other needs and both wait forever. Livelock is a similar situation where threads keep changing state but make no progress. Priority inversion occurs when a high-priority thread is blocked on a lock held by a low-priority thread. Even when these are avoided, the programmer must decide the granularity of locking: coarse-grained locks (one big lock for the whole data structure) are simple but serialize all access, defeating the purpose of concurrency; fine-grained locks (one lock per element) allow more parallelism but are harder to get right and can still deadlock. The fundamental difficulty is that the programmer must reason about all possible interleavings of the threads, and the number of interleavings grows exponentially with the number of threads and steps.
The shared-memory model is not a single "school" but the default hardware model, and its problems are not a matter of a wrong theory but of the difficulty of the task. It remains dominant because it is the most direct expression of what the hardware actually does, and because many applications, especially systems software, need the control it offers. Its limitations, however, have motivated most of the other approaches in the field.
A different tradition avoids shared memory entirely. In the message-passing model, concurrent processes do not share variables; they communicate only by sending and receiving messages. This is the model of Erlang, of the actor model (first described by Carl Hewitt and colleagues in the 1970s), and of many distributed systems. In the actor model, each actor is an independent computational entity with its own state, and it can only change that state in response to messages it receives. Actors can create other actors, send messages to actors whose addresses they know, and choose what to do with the next message. There is no shared state, so there is no need for locks. The programmer reasons about the system in terms of message flows and the order in which messages are processed, not about interleavings of memory accesses.
The actor model has a different set of strengths and weaknesses. It is naturally distributed: since actors do not share memory, they can be placed on different machines as easily as on different cores. It is also naturally fault-tolerant in the style of Erlang, where a failed actor can be restarted and its state rebuilt from messages. The main weakness is that message passing is often less efficient than shared memory for fine-grained communication, because copying or serializing a message is more expensive than reading a variable. Also, the programmer must design the message protocol carefully: there is no global view of the state, and reasoning about the system as a whole can be harder than reasoning about a single shared data structure. The actor model is not a single implementation but a family; Erlang's processes, the Akka library for Scala, and the Pony language are all actor-based, but they differ in details such as whether messages are ordered, whether actors can be supervised, and how the runtime schedules them.
Message passing is not a single "school" but a family of related models. A related but distinct tradition is communicating sequential processes (CSP), developed by Tony Hoare in the 1970s and 1980s, in which processes communicate by synchronous handshakes on named channels rather than by asynchronous messages. CSP is the basis of the Go language's goroutines and channels. In Go, a goroutine is a lightweight thread, and a channel is a typed pipe through which values are sent and received. The send and receive operations are synchronous: a send blocks until a receive is ready, and vice versa. This is a different discipline from the actor model: in CSP, the communication is the synchronization point, and the programmer composes processes by connecting their channels. The two models are often confused, but they have different semantics: actors are asynchronous and address-based, while CSP is synchronous and channel-based.
A third approach, software transactional memory (STM), tries to make shared-memory programming safer by borrowing an idea from database systems. In STM, the programmer writes a block of code that reads and writes shared variables, and the system treats that block as a transaction: it executes the block optimistically, as if it had exclusive access, and then at the end it checks whether any other thread has modified the data in the meantime. If so, the transaction is rolled back and retried. If not, it is committed. The programmer does not write locks; the system handles the conflict detection and retry. STM was proposed in the 1990s and became a research topic in the 2000s, with implementations in languages such as Haskell (in the STM monad) and Clojure (in its ref and atom types).
The appeal of STM is that it is compositional: a transaction can call another transaction, and the whole thing is still a transaction. This is something that locks cannot do easily: if a function acquires a lock and then calls another function that also acquires a lock, the two locks are not automatically combined into a single atomic unit. STM also avoids the deadlock problem, because there are no locks to hold. The main weakness is performance: the optimistic execution and conflict detection add overhead, and a transaction that is repeatedly retried can waste work. STM is also not a complete solution to all concurrency problems: it works well for data that is read and written in short, well-defined operations, but it is less natural for long-running interactions or for coordinating with external resources. STM is not a replacement for the other models but a different point in the design space, and it is often used in combination with them.
A more recent family of approaches, often grouped under the label structured concurrency, tries to make concurrency more like structured programming. The idea is that a concurrent program should have a clear hierarchy: a parent task creates child tasks, and the parent waits for the children to finish before it continues. This is in contrast to the "fire-and-forget" style of spawning a thread and letting it run independently, which makes it hard to know when a computation is complete or to cancel it. Structured concurrency is not a single language feature but a set of principles, popularized by the Kotlin language's coroutines and by the Java StructuredTaskScope API, among others. The key idea is that the lifetime of a child task is bounded by the lifetime of its parent, and that the parent can cancel or wait for the children in a controlled way.
A related but distinct idea is the data-theoretic approach, which includes dataflow and functional reactive programming (FRP). In a dataflow model, the program is a graph of nodes, and data flows along the edges. Each node is a pure function of its inputs, and the system automatically recomputes a node when its inputs change. This is the model of spreadsheet formulas, of the LabVIEW language, and of some modern systems like Differential Dataflow. The advantage is that the programmer does not specify the order of execution; the system does, based on the data dependencies. This makes the program deterministic: the same inputs produce the same outputs, regardless of scheduling. The disadvantage is that dataflow is not a good fit for all problems, especially those with complex control flow or with side effects. FRP is a related idea from the functional programming world, where the program is a description of how a value changes over time, and the system handles the propagation of changes.
These approaches are not mutually exclusive, and in practice they are often combined. A typical modern system might use shared memory for performance-critical data, with locks or STM for synchronization; use actors or channels for communication between components; and use structured concurrency to manage the overall lifecycle of tasks. The choice of model is not a matter of one being "right" and the others "wrong" but of the trade-offs that fit the problem. The shared-memory model is the most direct and the most difficult to reason about; the actor model is the most natural for distributed systems; STM is the most compositional for shared state; and structured concurrency is the most helpful for managing the lifecycle of tasks.
The field is also shaped by the hardware. The rise of multicore processors in the early 2000s made concurrency a mainstream concern, and the rise of cloud computing and distributed systems made the actor model and message passing more relevant. The memory model of a language—the formal specification of what orderings of memory operations are allowed—is a crucial part of the shared-memory model, and it is a subtle topic in its own right. The Java memory model, for example, is a formal specification that defines when one thread's writes are visible to another thread, and it is the basis for the correctness of Java's concurrency constructs. The C++ memory model is similar. These models are not just theoretical: they are the contract between the programmer and the compiler, and they determine what optimizations the compiler is allowed to make.
The current landscape of concurrency models is not a single "winner" but a set of coexisting approaches, each with its own strengths and weaknesses. The shared-memory model remains the foundation, because it is the model of the hardware and of the most widely used languages. The actor model and message passing are the standard for distributed systems and for fault-tolerant systems. STM is a promising but not yet dominant idea, with a strong presence in functional languages. Structured concurrency is a growing trend, especially in languages that support coroutines or fibers. The field is not a linear progression from "bad" to "good" but a set of trade-offs, and the choice of model is a matter of the problem, the language, and the programmer's preferences.
The most important lesson of the field is that concurrency is not a problem to be solved once and for all but a set of trade-offs to be managed. Each model makes some things easier and other things harder. The shared-memory model gives the programmer the most control but the most responsibility. The actor model gives the programmer a clean separation of concerns but requires careful protocol design. STM gives the programmer compositionality but at a performance cost. Structured concurrency gives the programmer a clear lifecycle but imposes a hierarchy that may not fit all problems. The field is a map of these trade-offs, and the skill of the programmer is to choose the right model for the problem at hand.