Recommended resources
The freely available resources this lesson will direct you to.
Hello! Welcome to the final module of our course on programming theory.
Introduction
In our last lesson, we explored how different programming paradigms approach data transformation. We saw that the functional pipeline model, with its emphasis on immutability and pure functions, is not just elegant but also inherently suited for parallel execution. This advantage is a direct consequence of avoiding the complexities that plague imperative code in concurrent settings.
Today, we will build directly on that insight to tackle one of the most challenging areas in software engineering: concurrency.
This lesson addresses the learning outcome: Evaluate paradigm choices for concurrency, contrasting functional approaches (e.g., actors in Erlang, immutability in Clojure) with traditional imperative models (e.g., threads and locks).
We will explore:
- The fundamental problem that makes concurrency so difficult.
- The traditional imperative solution using threads and locks.
- Two distinct functional strategies for sidestepping the problem entirely: one based on immutability (Clojure) and another on message-passing (Erlang's Actor Model).
By the end of this lesson, you will be able to analyze the trade-offs between these approaches and make informed decisions about which paradigm is best suited for a given concurrency problem.
1. The Root of All Evil: Shared Mutable State
Before we can compare solutions, we must understand the problem. Concurrency is difficult for one primary reason: managing shared mutable state.
Let's break that phrase down:
- State: Data stored in memory.
- Mutable: The data can be changed.
- Shared: More than one thread of execution can access the data at the same time.
When multiple threads try to read and write to the same piece of mutable data, chaos can ensue. This leads to classic concurrency bugs like race conditions, where the final state depends on the unpredictable timing of thread execution.
The 10 Common Concurrency Models by Jack Shirazi
To start, let's get a crisp definition of this core problem. The video 'The 10 Common Concurrency Models' by Jack Shirazi provides an excellent introduction.
Please watch the first 3 minutes (00:00 - 02:56). Focus on how the speaker defines 'shared mutable state' and identifies the two main strategies for dealing with it: either get rid of one of the words or use transactions.
As the video explains, virtually all concurrency models are designed to tackle this central problem. Let's first look at the most traditional approach.
2. The Imperative Model: Taming the Beast with Threads and Locks
The classic imperative approach, common in languages like C, C++, and early Java, accepts shared mutable state as a given and provides mechanisms to manage, or "tame," it.
The model is straightforward in theory:
- Threads: The operating system provides threads, which are independent execution paths that run within the same process and share the same memory space.
- Locks (Mutexes): To prevent race conditions, programmers use locks (short for "mutual exclusion") to protect "critical sections" of code where shared data is modified. A thread must acquire the lock before entering the critical section, and it releases the lock upon exiting, ensuring only one thread can modify the shared state at a time.
Concurrency, Parallelism, and their many forms
The article 'Concurrency, Parallelism, and their many forms' provides a clear overview of this model and its pitfalls.
Please read the section titled 'The Threads Model'. Pay attention to the description of how threads and mutexes work and the list of common problems: race conditions, deadlocks, and memory visibility issues.
While this gives programmers fine-grained control, it is notoriously difficult to get right. Deadlocks, where two or more threads are stuck waiting for each other to release locks, are a common and frustrating problem. Reasoning about the correctness of a complex multi-threaded program with explicit locking is a significant intellectual burden.
3. Functional Approaches: Changing the Rules of the Game
Functional programming takes a different tack. Instead of trying to manage shared mutable state, it aims to eliminate the problem by changing the rules. As the first video suggested, this is done by getting rid of either "mutable" or "shared."
Approach 1: Eliminate "Mutable" with Immutability (Clojure)
If data can never change, it can be freely and safely shared among any number of threads. There is no need for locks because there is no risk of one thread altering data while another is reading it. This is the core principle behind concurrency in many functional languages, with Clojure being a prime example.
Of course, applications need to manage changing state. Clojure and other functional languages handle this by creating new versions of data structures instead of modifying them in place. This might sound inefficient, but it's made practical by clever data structures called persistent data structures, which use structural sharing to make "updates" fast and memory-efficient.
how Clojure, Rust, Pony, Erlang and Dart let you achieve ...
The article 'Fearless Concurrency' explains how Clojure's focus on immutability provides a powerful model for safe concurrency.
Read the section 'Immutability, Functional Programming (Clojure)'. Focus on how state changes are handled with immutable data and how this model makes parallelizing operations trivial.
For situations that truly require managed, shared state, Clojure provides specific constructs like atoms which manage change via atomic compare-and-swap (CAS) operations, a more disciplined approach than manual locking.
Approach 2: Eliminate "Shared" with the Actor Model (Erlang)
The second functional strategy is to allow mutable state, but to never share it. This is the foundation of the Actor Model, famously implemented in Erlang and its ecosystem (including Elixir and Gleam).
In this model:
- The world consists of lightweight, independent processes called actors.
- Each actor has its own private state that no other actor can access directly.
- Actors communicate with each other exclusively by sending and receiving asynchronous messages.
- An actor processes messages from its mailbox one at a time, which prevents race conditions on its internal state.
This "share-nothing" architecture makes concurrency much easier to reason about. Since state is not shared, there is no need for locks. The model also scales transparently from multiple cores on one machine to a distributed network of machines.
A key feature of the Erlang/BEAM ecosystem is its philosophy of "let it crash," enabled by supervisors. Supervisors are special actors whose only job is to monitor other actors (workers) and restart them if they fail. This creates incredibly resilient, fault-tolerant systems.
The Ultimate Guide to Gleam Concurrency
This video on concurrency in Gleam (a modern language on the Erlang VM) provides an excellent, practical look at the Actor Model and supervisors.
Please watch the following three segments: Introduction to BEAM Concurrency (00:41 - 01:41): Understand the concept of lightweight, isolated processes with no shared memory. Actors for State Management (05:54 - 09:02): See how actors encapsulate state and handle messages. Supervisors for Fault Tolerance (09:36 - 13:06): Grasp the 'let it crash' philosophy and how supervision trees create resilient systems.
Your background in Multi-Agent Reinforcement Learning provides a strong conceptual parallel here. You can think of actors as agents that have their own internal state and policies, communicating with each other to achieve a collective goal.
4. Evaluating the Paradigms: Trade-offs and Choices
There is no single "best" concurrency model; the right choice depends on the problem. Let's evaluate the trade-offs.
The 10 Common Concurrency Models by Jack Shirazi
Let's return to Jack Shirazi's talk, where he discusses the practical challenges of these different models, particularly regarding maintenance and debugging.
Watch the section from 08:56 to 14:30. Pay close attention to why he argues there isn't a single 'winner' and the difficulties introduced by framework-heavy approaches (like complex stack traces).
Here is a summary of the trade-offs:
| Paradigm | Core Idea | Pros | Cons | Best For... |
|---|---|---|---|---|
| Threads & Locks (Imperative) | Manage access to shared mutable state. | - Fine-grained control - High performance if correct - Direct OS/hardware support |
- Extremely error-prone (deadlocks, race conditions) - Hard to reason about and debug - Does not scale to distributed systems |
Performance-critical systems programming on a single machine where complexity is manageable. |
| Immutability (Functional/Clojure) | Eliminate mutable state. | - Conceptually simple - No locks needed - Easy to parallelize data processing |
- Can have performance overhead from data copying - Can be awkward for modeling entities with long-lived, changing state |
Data processing pipelines, parallel computations, and systems where state changes are infrequent or can be clearly demarcated. |
| Actor Model (Functional/Erlang) | Eliminate shared state. | - Excellent for stateful services - Built-in fault tolerance - Scales from multi-core to distributed systems |
- Indirect communication can be complex - Debugging can be challenging (tracing messages) - Potential for mailbox overflow |
Highly available, scalable, and fault-tolerant systems like messaging platforms, financial trading systems, and IoT backends. |
Your work building financial systems at Revolut likely involved problems where the trade-offs between these models were critical. A trading platform needs high performance (favoring imperative ideas) but also extreme reliability and state management (favoring the Actor Model). Modern systems often blend these approaches.
Conclusion
Today we've contrasted the imperative and functional approaches to concurrency, revealing a fundamental difference in philosophy.
Key Takeaways:
- The core challenge of concurrency is managing shared mutable state.
- Imperative models (Threads & Locks) accept this challenge and provide tools to manage the complexity, at the cost of being difficult and error-prone.
- Functional models seek to avoid the problem by changing the rules:
- Immutability (Clojure-style) eliminates the "mutable," making data safe to share.
- The Actor Model (Erlang-style) eliminates the "shared," giving each process its own private state.
- The choice of paradigm is a significant architectural decision with deep implications for a system's scalability, reliability, and maintainability.
Preview of the Next Lesson:
We have now contrasted the imperative and functional paradigms on several fronts. In our final lesson, we will look at a language that represents a fascinating synthesis of these ideas: Rust. We will analyze how Rust's unique ownership model provides compile-time guarantees against concurrency bugs, integrating ideas from functional, imperative, and systems programming to offer a novel solution to the problem of "fearless concurrency."