Threads
A thread is one path of work through a program. One process can run several threads, and they all share the same memory. So both threads here can read and change the same variable: count.
With one CPU core, the operating system switches the core between the threads many times a second. Each thread runs a little, then waits. That is concurrency: both make progress, by interleaving, never at the same instant.
With two cores, each thread can run on its own core at the very same moment. That is parallelism. Either way, the threads share memory, and their small steps can overlap in any order.
A race
count += 1 looks like one action, but it runs as three: read count into a private scratch value, tmp; add 1 to tmp; write tmp back into count. Another thread can run between any two of them.
Both threads run count += 1, so count should end at 2. Thread A goes first: it reads count and gets 0. Its private tmp is now 0.
Before A writes anything back, the switch lands on thread B. B reads count as well. It is still 0, so B’s tmp is 0 too. Both threads now hold the same old value.
Thread A adds 1 to its tmp and writes 1 into count. So far so good: one increment is done.
Thread B adds 1 to its old 0 and writes 1 too, over A’s result. count is 1, not 2: one update is lost. This bug is a race condition: the result depends on timing.
Locks
The fix is a lock, also called a mutex (mutual exclusion). Starting again from 0, thread A takes the lock, then reads, adds and writes: count becomes 1. B asks for the lock too and must wait.
When A finishes, it releases the lock and B takes it. B reads 1, adds 1 and writes 2. The three steps of the two threads can no longer mix, so count always ends at 2.
Locks bring a new danger. Thread A holds lock 1 and waits for lock 2. Thread B holds lock 2 and waits for lock 1. Each waits for the other, so neither ever moves. A common cure: always take locks in the same order.
Python
Standard CPython has a global interpreter lock, the GIL: only one thread runs Python code at a time, even with many cores. So threads that only compute take turns. Python 3.13 added an optional free-threaded build without it.
The Python behind it
;; step 1/12 · One process, two threads
import threading
count = 0 # shared by every thread
def add():
global count
count += 1
a = threading.Thread(target=add)
b = threading.Thread(target=add)
a.start(); b.start() # both run add()
a.join(); b.join() # wait for both
If the six small steps of two threads could interleave in any order, 18 of the 20 possible orders would lose an update. Only the two orders in which one thread finishes before the other starts give 2. The free-threaded build (PEP 703) was experimental in Python 3.13 and became officially supported in 3.14 (PEP 779). It is a separate build; the standard build still has the GIL. Without the GIL, a lock is still needed for count += 1.