From two compiles to the cores
One python main.py, one address space. Two compiles fill its regions, only one is contested.
// CPython eval loop
for (;;) {
op = *ip++;
Py_INCREF(x); // rc++
Py_DECREF(y); // rc--
}python.c
→gcc
01001010
11010100
00101101
10100101
...
CPythonthe interpreter
⇒
from threading import Thread
def work(): # CPU-bound
while True: crunch()
for _ in range(3):
Thread(target=work).start()main.py
→CPython
LOAD_GLOBAL
LOAD_FAST
CALL
STORE_FAST
...
.pyc
⇒
RAM · one address space
Textshared · read-only
binary
Datashared · read-only
None−5…256type
Heapshared · mutable
bytecodercintrc 1listrcModel()rc
▼the Heap holds the shared, mutable objects, so it is where threads contend

main.py
you call Thread(...).start() three times
→

the OS
makes 3 kernel threads (1:1), each its own native stack, runnable on any core
→

CPython
3 runnable threads share one heap — which may execute bytecode?
CPython's two answers to protecting the shared heap:
CPython ≤ 3.12With the GIL
🔒the eval loop runs under the GIL
GIL held by T1 — the rest wait
T1core 0
—core 1
—core 2
—core 3
one global lock
CPython 3.13+Free-threaded
no GIL — threads run bytecode in parallel
all three advance together
T1core 0
T2core 1
T3core 2
—core 3
fine-grained locking