Concurrency in Computer Science
Introduction to Concurrency
Concurrency is a fundamental concept in computer science that allows multiple tasks or processes to make progress independently. It is the basis of responsive UIs, high-throughput servers, and efficient use of modern multi-core CPUs.
Concurrency is not the same as parallelism:
- Concurrency — multiple tasks are in progress at the same time (they may take turns on one CPU)
- Parallelism — multiple tasks execute at the exact same instant on multiple CPU cores
An event loop handling 10,000 connections on one thread is concurrent but not parallel. Four threads crunching different sections of a matrix are parallel.
Why Concurrency Matters
- Resource utilization: While one task waits on disk I/O or a network response, another task can run. The CPU is never idle for no reason.
- Responsiveness: In UI applications, background work (file load, network call) runs concurrently so the UI thread remains responsive.
- Throughput: Web servers handle thousands of simultaneous requests by processing them concurrently rather than one at a time.
Models of Concurrency
1. Thread-Based Concurrency
Threads share the same process memory. The OS scheduler decides which thread runs when.
import threading
counter = 0
def increment():
global counter
for _ in range(100_000):
counter += 1 # NOT thread-safe
t1 = threading.Thread(target=increment)
t2 = threading.Thread(target=increment)
t1.start(); t2.start()
t1.join(); t2.join()
print(counter) # Not 200,000 — race condition!
The result is unpredictable because counter += 1 is three operations (read, add, write) that can interleave between threads.
With a lock:
lock = threading.Lock()
def safe_increment():
global counter
for _ in range(100_000):
with lock:
counter += 1
# Now always prints 200,000
2. Process-Based Concurrency
Processes have separate memory spaces. Data is copied or passed via IPC (pipes, sockets, shared memory). This avoids race conditions on shared data but makes communication more expensive.
from multiprocessing import Process, Value
def worker(shared_val, n):
for _ in range(n):
with shared_val.get_lock():
shared_val.value += 1
shared = Value('i', 0)
p1 = Process(target=worker, args=(shared, 100_000))
p2 = Process(target=worker, args=(shared, 100_000))
p1.start(); p2.start()
p1.join(); p2.join()
print(shared.value) # 200,000
In Python, multiprocessing bypasses the GIL, making it the standard approach for CPU-bound parallel work.
3. Asynchronous Programming
Instead of blocking on I/O, the program registers a continuation and the event loop calls it when the result is ready. One thread handles many operations concurrently.
import asyncio
async def fetch(url):
# Simulates a network call without blocking
await asyncio.sleep(1)
return f"data from {url}"
async def main():
# Both fetches run concurrently — total time ~1s, not ~2s
results = await asyncio.gather(
fetch("https://api.example.com/users"),
fetch("https://api.example.com/orders"),
)
print(results)
asyncio.run(main())
4. Actor Model
Actors are independent units that communicate only by sending immutable messages — no shared state. Erlang, Akka (JVM), and Orleans (C#) implement this model.
Actor A —[message: "fetch user 42"]→ Actor B (DB worker)
↓
Actor A ←[message: "user: {id: 42, ...}"]— Actor B
Since actors never share memory directly, race conditions are structurally impossible.
5. Go's Goroutines and Channels
Go takes a different approach: goroutines (lightweight threads, ~2 KB stack) scheduled by the Go runtime, and channels for safe communication.
package main
import (
"fmt"
"sync"
)
func worker(id int, jobs <-chan int, results chan<- int, wg *sync.WaitGroup) {
defer wg.Done()
for j := range jobs {
results <- j * j // square the input
}
}
func main() {
jobs := make(chan int, 10)
results := make(chan int, 10)
var wg sync.WaitGroup
for w := 1; w <= 3; w++ {
wg.Add(1)
go worker(w, jobs, results, &wg)
}
for j := 1; j <= 9; j++ {
jobs <- j
}
close(jobs)
go func() {
wg.Wait()
close(results)
}()
for r := range results {
fmt.Println(r)
}
}
Go's mantra: "Do not communicate by sharing memory; instead, share memory by communicating."
Common Concurrency Problems
Race Conditions
A race condition occurs when the result depends on the order of uncoordinated thread execution. See the counter example above. Fixes: locks, atomic operations, channels, immutable data.
Deadlocks
Two or more threads each hold a resource and wait for the other to release theirs — neither ever proceeds.
import threading
lock_a = threading.Lock()
lock_b = threading.Lock()
def thread1():
with lock_a:
print("T1 acquired A, waiting for B")
with lock_b: # waits forever if T2 holds B
print("T1 acquired B")
def thread2():
with lock_b:
print("T2 acquired B, waiting for A")
with lock_a: # waits forever if T1 holds A
print("T2 acquired A")
# Run these two threads simultaneously → deadlock
Prevention: Always acquire locks in a consistent global order. Use timeouts (lock.acquire(timeout=1.0)). Prefer higher-level primitives that avoid manual locking.
Starvation
A thread is repeatedly denied CPU time because higher-priority threads keep running. Common in poorly designed priority schedulers. Fixed by aging (gradually increasing the priority of waiting threads) or fair-queuing algorithms.
Livelock
Threads are not blocked but keep changing state in response to each other without making progress — like two people stepping aside in a corridor only to find they're both now in each other's way again.
Concurrency Model Comparison
| Model | Memory sharing | Communication | Best for |
|---|---|---|---|
| Threads | Yes (explicit sync) | Shared vars / locks | CPU-bound, low contention |
| Processes | No (separate spaces) | IPC, pipes, sockets | CPU-bound, isolation needed |
| Async / event loop | Yes (single-threaded) | Callbacks / await | I/O-bound, many connections |
| Actor model | No (message passing) | Immutable messages | Distributed systems, complex state machines |
| Goroutines + channels | Limited (channels) | Channels | I/O + CPU, Go ecosystem |
Language Support Summary
| Language | Threads | Async | Green threads / coroutines | Notes |
|---|---|---|---|---|
| Python | Yes (GIL limits CPU) | asyncio | gevent | Use multiprocessing for CPU-bound |
| Go | Yes (goroutines) | Goroutines | Native | Best-in-class built-in concurrency |
| Java | Yes | CompletableFuture, virtual threads (21+) | Project Loom | Virtual threads now mainstream |
| JavaScript | No (single-threaded) | async/await, Promises | Workers API | I/O-only; Web Workers for CPU |
| Rust | Yes | async/await (tokio) | — | Memory safety eliminates data races at compile time |
