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
Stackprivate
T1
T2
T3
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
T1
ldincstjmp
T2
ldincstjmp
T3
ldincstjmp
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
T1
ldincstjmp
T2
ldincstjmp
T3
ldincstjmp
all three advance together
T1core 0
T2core 1
T3core 2
core 3
fine-grained locking