Что на самом деле делает GIL. В каждый момент байткод выполняет один поток, но переключение возможно между любыми инструкциями (по умолчанию каждые 5 мс). counter += 1 компилируется примерно в LOAD, BINARY_OP +=, STORE — два потока читают 5, оба пишут 6.
import threading
counter = 0
lock = threading.Lock()
def inc():
global counter
for _ in range(100_000):
with lock:
counter += 1
Без with lock итог при нескольких потоках может быть меньше ожидаемого.
Атомарность в CPython. Одиночные операции, реализованные одной инструкцией на C (list.append, dict[key] = value, deque.popleft), атомарны благодаря GIL. Но полагаться на это хрупко: сочетание двух атомарных операций уже не атомарно, а в сборке без GIL гарантии обеспечиваются по-другому.
Инструменты: Lock, RLock (реентерабельный), Condition, Semaphore, Event; queue.Queue — потокобезопасная очередь, лучший способ передавать работу между потоками.