Concurrent Programming
Table of Contents
- Concurrent Programming
What’s Concurrent Programming
Concurrent programming is a programming paradigm that deals with the execution of multiple tasks or processes simultaneously. It enables programs to handle multiple operations at the same time, improving responsiveness, resource utilization, and performance.
Key Characteristics:
- Multiple execution paths: Different parts of a program execute independently
- Coordination: Tasks must coordinate access to shared resources
- Non-deterministic: The exact order of operations may vary between executions
- Interleaving: Tasks may switch execution at any point
Unlike sequential programming where instructions execute one after another, concurrent programming allows multiple sequences of operations to make progress during overlapping time periods.
Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once. - Rob Pike
Concurrency vs Parallelism
While often used interchangeably, concurrency and parallelism are distinct concepts:
Concurrency
Concurrency is about structure - the composition of independently executing tasks.
- Focus: Managing multiple tasks at once
- Execution: Tasks can run on a single processor (time-slicing)
- Goal: Better program structure, responsiveness
- Example: A web server handling multiple requests by switching between them
Time →
CPU: [Task A]--[Task B]--[Task A]--[Task C]--[Task B]
↑ Context switching between tasks
Parallelism
Parallelism is about execution - the simultaneous execution of multiple tasks.
- Focus: Actually running multiple tasks at the same instant
- Execution: Requires multiple processors/cores
- Goal: Performance improvement through simultaneous execution
- Example: Matrix multiplication split across multiple CPU cores
Time →
CPU 1: [Task A--------------]
CPU 2: [Task B--------------]
CPU 3: [Task C--------------]
↑ True simultaneous execution
Relationship:
- Concurrency enables parallelism (but doesn’t require it)
- Parallel programs must handle concurrency concerns
- Concurrent programs may or may not execute in parallel
Core Concepts
Processes and Threads
Process:
- Independent execution unit with its own memory space
- Isolated from other processes
- Heavy-weight (significant overhead to create)
- Communication via inter-process communication (IPC)
Thread:
- Lightweight execution unit within a process
- Shares memory space with other threads in the same process
- Lower overhead than processes
- Easier communication but requires synchronization
┌─────────────────────────────────────┐
│ Process │
│ ┌──────────┐ ┌──────────┐ │
│ │ Thread 1 │ │ Thread 2 │ │
│ └──────────┘ └──────────┘ │
│ │
│ ┌────────────────────────┐ │
│ │ Shared Memory │ │
│ └────────────────────────┘ │
└─────────────────────────────────────┘
Thread Safety
A piece of code is thread-safe if it functions correctly when accessed by multiple threads simultaneously.
Thread-safe requirements:
- No race conditions
- Proper synchronization of shared state
- Consistent state transitions
- No data corruption
Common thread-safety techniques:
- Synchronization: Using locks/mutexes
- Immutability: Using immutable data structures
- Thread-local storage: Each thread has its own copy
- Atomic operations: Indivisible operations
- Lock-free algorithms: Using atomic compare-and-swap
Synchronization
Synchronization is the coordination of concurrent tasks to ensure correct execution.
Synchronization Mechanisms:
- Mutex (Mutual Exclusion)
- Ensures only one thread accesses a resource at a time
- Lock before access, unlock after
- Semaphore
- Controls access to a resource pool
- Allows N threads to access simultaneously
- Monitor
- Combines mutex with condition variables
- Wait/notify mechanisms
- Barriers
- Synchronization point where all threads must arrive before proceeding
- Read-Write Locks
- Multiple readers or single writer
- Optimizes for read-heavy workloads
Race Conditions
A race condition occurs when the program’s behavior depends on the relative timing of events.
Example:
Thread A: Thread B:
read balance (100) read balance (100)
add 50 add 30
write balance (150) write balance (130) ← Lost update!
Solution: Synchronize access to shared state.
Deadlock
Deadlock occurs when two or more threads are blocked forever, each waiting for the other to release a resource.
Four necessary conditions (Coffman conditions):
- Mutual exclusion: Resources cannot be shared
- Hold and wait: Threads hold resources while waiting for others
- No preemption: Resources cannot be forcibly taken
- Circular wait: Circular chain of waiting threads
Example:
Thread A: Thread B:
lock(Resource 1) lock(Resource 2)
lock(Resource 2) ←─┐ lock(Resource 1) ←─┐
│ │
└───── DEADLOCK ────────┘
Prevention strategies:
- Lock ordering (always acquire locks in same order)
- Lock timeout (give up if lock not acquired in time)
- Deadlock detection and recovery
- Avoid nested locks when possible
Shared State vs Message Passing
Two fundamental approaches to concurrent programming:
Shared State (Shared Memory)
Threads communicate by reading/writing shared variables.
Advantages:
- Direct access to shared data
- Low overhead for simple cases
- Familiar to most programmers
Disadvantages:
- Requires explicit synchronization
- Prone to race conditions
- Difficult to reason about
- Scalability challenges
Example paradigms: Traditional multi-threading, OpenMP
Message Passing
Threads communicate by sending messages (no shared state).
Advantages:
- No shared state = no race conditions
- Easier to reason about
- Natural for distributed systems
- Better scalability
Disadvantages:
- Message passing overhead
- May require data copying
- Different programming model
Example paradigms: Actor model (Erlang, Akka), Go channels, MPI
Concurrency Models
Thread-Based Concurrency
The traditional model where the operating system manages threads.
Characteristics:
- Preemptive multitasking
- OS-scheduled
- Can utilize multiple cores
- Requires synchronization primitives
Use cases:
- General-purpose concurrent programming
- CPU-intensive parallel tasks
- Server applications
Languages: Java, C++, C#, Python (with GIL limitations)
Event-Driven Concurrency
Single-threaded event loop processing events from a queue.
Characteristics:
- Non-blocking I/O
- Callbacks or async/await
- Single-threaded (usually)
- Cooperative multitasking
Use cases:
- I/O-heavy applications
- Web servers (Node.js)
- UI programming
Languages: JavaScript/Node.js, Python (asyncio), Rust (tokio)
Actor Model
Independent actors communicate via asynchronous messages.
Characteristics:
- No shared state
- Each actor has a mailbox
- Message-based communication
- Location transparency
Principles:
- Actors process one message at a time
- Actors can create other actors
- Actors can send messages to other actors
- Supervision hierarchies for fault tolerance
Use cases:
- Distributed systems
- Fault-tolerant applications
- High-concurrency scenarios
Languages: Erlang, Elixir, Akka (Scala/Java), Pony
Coroutines
Lightweight, cooperative concurrent tasks.
Characteristics:
- User-space scheduling
- Explicit yield points
- Very low overhead
- Can have millions of coroutines
Use cases:
- Async I/O operations
- Game engines
- High-concurrency servers
Languages: Kotlin, Go (goroutines), Python (async/await), Lua
Common Patterns
Producer-Consumer
One or more producers create work items; one or more consumers process them.
Components:
- Producers: Generate data/tasks
- Queue: Buffers items between producers and consumers
- Consumers: Process data/tasks
Benefits:
- Decouples production from consumption
- Balances different processing speeds
- Enables parallel processing
Thread Pool
Reusable pool of worker threads that execute submitted tasks.
Benefits:
- Avoids thread creation overhead
- Limits resource consumption
- Better CPU utilization
- Task queue management
Components:
- Worker threads (fixed or dynamic size)
- Task queue
- Submission interface
- Lifecycle management
See also: Java Executors
Future/Promise
Placeholder for a value that will be available in the future.
Characteristics:
- Represents asynchronous computation result
- Can be queried for completion status
- Blocks when retrieving result (if not ready)
- Enables composition of async operations
Variants:
- Future: Read-only view of async result
- Promise: Writable future (one-time)
- CompletableFuture: Composable async operations
See also:
Lock-Free Data Structures
Data structures that use atomic operations instead of locks.
Techniques:
- Compare-and-swap (CAS)
- Load-link/store-conditional
- Memory barriers
Benefits:
- No deadlock risk
- Better performance under contention
- Progress guarantees (wait-free, lock-free)
Challenges:
- Complex to implement correctly
- ABA problem
- Memory ordering concerns
Benefits and Challenges
Benefits
- Improved Responsiveness
- UI remains responsive during long operations
- Better user experience
- Resource Utilization
- Utilize multiple CPU cores
- Overlap I/O with computation
- Better throughput
- Performance
- Parallel execution on multi-core systems
- Reduced latency for I/O operations
- Scalability
- Handle more concurrent users/requests
- Efficient use of system resources
- Modularity
- Natural decomposition of problems
- Independent components
Challenges
- Complexity
- Harder to design and understand
- Non-deterministic behavior
- Difficult to test and debug
- Race Conditions
- Subtle timing-dependent bugs
- Hard to reproduce and fix
- Deadlocks
- System hangs completely
- Can be difficult to detect
- Synchronization Overhead
- Lock contention reduces performance
- Context switching costs
- Memory Consistency
- Visibility of shared state
- Memory model complexities
- Cache coherence issues
- Testing Difficulties
- Non-deterministic execution
- Heisenbugs (disappear when debugging)
- Need for stress testing
See also: Java Memory Model
Examples
Thread Creation (Multiple Languages)
Java:
// Using Thread class
Thread thread = new Thread(() -> {
System.out.println("Running in thread: " + Thread.currentThread().getName());
});
thread.start();
// Using ExecutorService (recommended)
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> {
System.out.println("Task executed by: " + Thread.currentThread().getName());
});
executor.shutdown();
Python:
import threading
def worker():
print(f"Running in thread: {threading.current_thread().name}")
# Create and start thread
thread = threading.Thread(target=worker)
thread.start()
thread.join() # Wait for completion
Go:
package main
import (
"fmt"
"sync"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d executing\n", id)
}
func main() {
var wg sync.WaitGroup
for i := 1; i <= 5; i++ {
wg.Add(1)
go worker(i, &wg) // Launch goroutine
}
wg.Wait() // Wait for all goroutines
}
Rust:
use std::thread;
fn main() {
let handle = thread::spawn(|| {
println!("Running in thread");
});
handle.join().unwrap(); // Wait for thread
}
Synchronization Example
Problem: Multiple threads incrementing a shared counter (unsafe).
Java (with synchronization):
public class Counter {
private int count = 0;
// Synchronized method ensures thread safety
public synchronized void increment() {
count++;
}
// Or using explicit lock
private final Object lock = new Object();
public void incrementWithLock() {
synchronized (lock) {
count++;
}
}
// Modern approach: AtomicInteger
private AtomicInteger atomicCount = new AtomicInteger(0);
public void incrementAtomic() {
atomicCount.incrementAndGet(); // Lock-free!
}
}
Python (with lock):
import threading
class Counter:
def __init__(self):
self.count = 0
self.lock = threading.Lock()
def increment(self):
with self.lock:
self.count += 1
Go (with mutex):
type Counter struct {
mu sync.Mutex
count int
}
func (c *Counter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
Message Passing Example
Go (channels):
func main() {
messages := make(chan string)
// Producer goroutine
go func() {
messages <- "ping"
}()
// Consumer (main goroutine)
msg := <-messages
fmt.Println(msg) // "ping"
}
Erlang (actor model):
% Spawn a process that receives messages
receiver() ->
receive
{From, Message} ->
io:format("Received: ~p~n", [Message]),
From ! {self(), "acknowledged"},
receiver() % Tail recursion
end.
% Send a message to the receiver
Pid = spawn(fun receiver/0),
Pid ! {self(), "Hello"}.
Rust (channels):
use std::sync::mpsc;
use std::thread;
fn main() {
let (tx, rx) = mpsc::channel();
thread::spawn(move || {
tx.send("Hello from thread").unwrap();
});
let received = rx.recv().unwrap();
println!("{}", received);
}
Languages
Modern programming languages provide varying levels of support for concurrent programming:
Native Concurrency Support
- Go: Goroutines and channels as first-class features
- Lightweight coroutines (goroutines)
- Channel-based message passing
- Simple concurrent programming model
- Erlang/Elixir: Actor model built into the language
- Lightweight processes (actors)
- Supervision trees for fault tolerance
- Designed for high-concurrency systems
- Rust: Ownership model prevents data races at compile time
- Thread safety guaranteed by type system
- Fearless concurrency
- Zero-cost abstractions
Strong Standard Library Support
- Java: Comprehensive concurrency utilities
- See also: Java Concurrency
- Thread API, ExecutorService, concurrent collections
- CompletableFuture for async programming
- Fork/Join framework for parallelism
- Detailed memory model for thread visibility
- C#: Task Parallel Library (TPL) and async/await
- Tasks and async/await keywords
- PLINQ for parallel queries
- Concurrent collections
- Python: Threading and multiprocessing modules
- threading module (limited by GIL)
- multiprocessing for true parallelism
- asyncio for async/await concurrency
- JavaScript/TypeScript: Event loop and async/await
- Single-threaded event loop
- Promises and async/await
- Web Workers for parallelism
Library/Framework Support
- Scala: Akka framework for actor-based concurrency
- Futures and actors
- Akka toolkit for distributed systems
- C/C++: Pthreads, C++11 threading, OpenMP
- Manual thread management
- Low-level primitives
- High performance
- Kotlin: Coroutines for structured concurrency
- Lightweight coroutines
- Flow for reactive streams
- Integration with Java concurrency
Ref.
Books:
- Java Concurrency in Practice - Brian Goetz (applicable concepts beyond Java)
- The Art of Multiprocessor Programming - Herlihy & Shavit
- Seven Concurrency Models in Seven Weeks - Paul Butcher
Articles and Papers:
- Concurrency is not Parallelism - Rob Pike
- Communicating Sequential Processes - Tony Hoare (CSP paper)
Online Resources:
- Operating Systems: Three Easy Pieces - Concurrency
- The Little Book of Semaphores - Allen Downey
- Patterns for Parallel Programming
Language-Specific:
- Java: Java Concurrency Documentation
- Go: Effective Go - Concurrency
- Rust: The Rust Book - Concurrency
- Erlang: Learn You Some Erlang - Concurrency
Related Paradigms:
- Reactive Programming - Event-driven asynchronous programming
- Functional Programming - Immutability aids concurrent programming