🐍 Python Course · Stage 4 · Lesson 71 of 89 · Concept: Concurrency
Course Overview →

🟩 These concept pages connect Python to programming in general. Read Beginner tab first.

← Concept: Modules Concept: Recursion →
thecodex.expert · The Codex Family of Knowledge
CS Concepts

Concurrency

Multiple tasks making progress at the same time — the challenge underlying every web server, GUI, and distributed system.

CS Concepts Language-independent ~18 min read Last verified:
Canonical Definition

Concurrency is the ability of a program to have multiple computations in progress at the same time — interleaving on one CPU core or running in parallel on multiple cores — requiring synchronisation mechanisms to prevent conflicts over shared mutable state.

🟩 Beginner

Doing more than one thing — the concept

What you'll learn: Why concurrency matters (slow I/O), the difference between threads (shared memory) and async/await (cooperative), and what a race condition is.

How to read this tab: The Python-specific models (threading, multiprocessing, asyncio) are at Concurrency in Python. This page explains the universal concepts.

⏱ 20 min 📄 4 sections 🔶 Prerequisite: Functions + Scope
One sentence that captures it

Concurrency is a program having multiple things in progress at the same time — whether truly running simultaneously on different CPU cores, or taking turns so quickly it appears simultaneous.

Concurrency exists because computers spend enormous time waiting. A web server waiting for a database query. A script waiting for a network response. A game waiting for user input. Without concurrency, the CPU sits idle during that wait. With concurrency, other work happens while waiting — multiplying effective throughput without multiplying CPU cores.

Why it matters

A web server handling 1000 requests per second cannot process them one at a time. A GUI cannot freeze while downloading a file. Concurrency solves these problems: multiple tasks appear to make progress simultaneously.

Shared memory means shared bugs. Threads can read and write the same variables — which is powerful but dangerous. If two threads modify the same counter at the same time, both read the old value, both add 1, and both write the same result. The counter increments once instead of twice. This is a race condition. Preventing them requires synchronisation, which introduces deadlocks. Concurrency is hard.

Threads: sharing memory

A thread is an independent sequence of execution within a process. Threads share the same memory. Fast communication — but shared mutation introduces race conditions: two threads modifying the same data simultaneously can corrupt it.

Pythonthreads.py
import threading

results = []   # shared — race condition risk

def worker(n):
    results.append(n * n)

threads = [threading.Thread(target=worker, args=(i,)) for i in range(5)]
for t in threads: t.start()
for t in threads: t.join()  # wait for all
print(sorted(results))      # [0, 1, 4, 9, 16]
💡

Async/await avoids shared state. In async programming, only one coroutine runs at a time — it explicitly yields control at await points. No two pieces of code run simultaneously, so there's no race condition on shared data. The trade-off: you must await every I/O operation, and one blocking (non-await) call stalls everything. Works best for high-volume I/O-bound work.

Async/await: I/O concurrency without threads

For I/O-bound work (network, file, database), async/await is often better. An async function pauses at await and lets other tasks run during the wait — on a single thread. Python asyncio, JavaScript Promises, Rust async.

Pythonasync.py
import asyncio

async def fetch(url):
    await asyncio.sleep(1)   # simulate 1-second network call
    return f"data from {url}"

async def main():
    # Both fetches run concurrently — total ~1 second, not 2
    results = await asyncio.gather(
        fetch("https://api1.example.com"),
        fetch("https://api2.example.com"),
    )
    print(results)

asyncio.run(main())

Race conditions are invisible, intermittent, and devastating. The code looks correct. It works 99% of the time. Occasionally, two threads interleave in exactly the wrong way and data is corrupted. These bugs are notoriously hard to reproduce and debug. The solution: never share mutable state between threads without a lock, or don't share mutable state at all (use queues to pass data between threads).

Race conditions

Two threads read balance = 1000, both add 100, both write 1100. The correct result should be 1200. Race conditions are hard to reproduce because they depend on exact execution timing. The solution: synchronisation — allow only one thread to modify shared state at a time.

Rob Pike's definition: "Concurrency is about dealing with lots of things at once. Parallelism is about doing lots of things at once." Concurrency is a program structure — multiple tasks that can overlap. Parallelism is physical execution — multiple tasks running simultaneously on multiple cores. A concurrent program on one core is not parallel. A parallel program requires multiple cores.

Concurrency vs. parallelism

Concurrency: multiple tasks in progress (may interleave on one CPU core). Parallelism: multiple tasks running simultaneously on multiple cores. Concurrency is a program structure; parallelism is a hardware reality.

✅ Beginner tab complete — check your understanding

  • I can explain why concurrency exists: to use waiting time productively
  • I know what a thread is: an independent execution path sharing the same memory
  • I know what a race condition is: two threads modifying the same data simultaneously with unpredictable results
  • I understand async/await at a high level: one thread, cooperative switching at await points

Continue to Recursion →

🔵 Intermediate

Concurrency vs parallelism, the GIL, and synchronisation

What you'll learn: The precise difference between concurrency (dealing with multiple things at once) and parallelism (doing multiple things simultaneously). Python's GIL. Synchronisation primitives: locks, semaphores, and queues.

How to read this tab: The concurrency vs parallelism distinction is one of the most commonly confused concepts in programming. Get it clear here, then it applies to every language you work in.

⏱ 20 min 📄 2 sections 🔶 Prerequisite: Beginner tab
📎

Python's GIL is a mutex on the interpreter itself. Only one thread executes Python bytecode at a time. This makes the interpreter simpler and safe from data corruption, but means Python threads don't run Python code in parallel. The GIL IS released during I/O — which is why threads still help I/O-bound code. For CPU-bound parallelism, use multiprocessing (separate processes, each with their own GIL).

Python's Global Interpreter Lock

CPython has a GIL — a mutex ensuring only one thread executes Python bytecode at a time. Multi-threaded Python cannot achieve true CPU parallelism. I/O threads release the GIL while waiting, so threading helps for I/O. For CPU parallelism: multiprocessing (separate processes) or C extensions that release the GIL (NumPy operations do this).

📎

Locks, semaphores, events, and queues — the tools for safe concurrency. Lock: ensures only one thread in a block at a time. Semaphore: allows N threads in a block at a time. Event: lets one thread signal another. Queue (thread-safe): pass data between threads without shared state. Python's threading module provides all of these. The safest: communicate via Queue, avoid shared mutable state entirely.

Synchronisation primitives

  • Lock/Mutex: only one thread holds it at a time — ensures mutual exclusion.
  • Semaphore: allows up to N threads simultaneously.
  • Condition variable: threads wait until a condition is true.
  • Atomic operation: single uninterruptible hardware instruction (compare-and-swap, fetch-and-add).
Pythonlocking.py
import threading

balance = 0
lock = threading.Lock()

def deposit(amount):
    global balance
    with lock:           # acquire; release on exit
        balance += amount

threads = [threading.Thread(target=deposit, args=(100,)) for _ in range(100)]
for t in threads: t.start()
for t in threads: t.join()
print(balance)   # 10000 — correct; race condition eliminated

The actor model

Alternative to shared memory: actor model (Hewitt, 1973). Each actor has private state and communicates only by messages — no shared memory, no race conditions by design. Erlang and Elixir are built on this model. Go: "do not communicate by sharing memory; share memory by communicating" — goroutines + channels.

Commonly confused
Concurrent ≠ parallel. Concurrency = multiple tasks in progress (may interleave on one core). Parallelism = running simultaneously on multiple cores. A single-core machine can be concurrent but not parallel.
The GIL does not make Python thread-safe. The GIL prevents simultaneous bytecode execution, but a thread can still be interrupted mid-operation. balance += amount compiles to multiple bytecodes and is not atomic — a race condition is still possible without explicit locking.
Async/await is not multithreading. Python asyncio runs on a single thread. Only one coroutine runs at any moment. Async is about cooperative I/O waits, not CPU parallelism.
How this connects
Requires first
Enables
Confused with

✅ Intermediate tab complete — check your understanding

  • I can explain the difference: concurrency (structure) vs parallelism (execution). A single-core CPU can be concurrent but not parallel.
  • I know Python's GIL prevents true parallelism for CPU-bound Python code
  • I know what a mutex (lock) is: a primitive that ensures only one thread accesses a resource at a time

Continue to Recursion →

🔴 Expert

The actor model, memory models, and compare-and-swap

What you'll learn: The actor model (Erlang, Elixir, Akka) as an alternative to shared-memory concurrency. Memory models and happens-before relationships. Compare-and-swap for lock-free programming.

How to read this tab: Read when studying distributed systems, Rust concurrency, or language implementation.

⏱ 20 min 📄 3 sections 🔶 Prerequisite: After Stage 2

Memory models and happens-before

In a multi-core system, each core has its own cache. A write on Core 1 may not be immediately visible to Core 2. The Java Memory Model (JLS §17) specifies happens-before relationships that guarantee visibility. The C++11 memory model (ISO C++ §29) defines six memory orderings from seq_cst (strongest) to relaxed (weakest/fastest).

Compare-and-swap and lock-free programming

Compare-and-swap (CAS): atomically compare a memory location with an expected value; if equal, replace with a new value. The foundation of lock-free data structures. Java: compareAndSet() in java.util.concurrent.atomic. Rust: compare_exchange() in std::sync::atomic. Lock-free algorithms avoid mutex overhead and deadlock but are significantly harder to reason about.

Specification reference

Java Memory Model: JLS §17. Python GIL: docs.python.org/3/glossary.html. C++11 atomics: ISO C++ §29. Hewitt, C. (1973). Actor model. IJCAI '73.

✅ Expert tab complete

  • I can explain the actor model: isolated state, message passing, no shared memory
  • I know what a happens-before relationship is in memory models
  • I understand compare-and-swap as the foundation of lock-free data structures

Continue to Recursion →

Sources

1
Java Language Specification §17 — Threads and Locks. Oracle. docs.oracle.com/javase/specs/.
2
Python Software Foundation. Global Interpreter Lock. docs.python.org/3/glossary.html.
3
ISO/IEC 14882:2011 (C++11). §29 — Atomic operations.
4
Hewitt, C. (1973). "A Universal Modular ACTOR Formalism for Artificial Intelligence." IJCAI '73.
Source confidence: High Last verified: Primary source: Java Language Specification §17 — Threads and Locks