GIL v CPythonu: mechanismus, omezení a způsoby obejití pro seniorní vývojáře
V CPythonu GIL chrání reference counting před race condition. Bez něj by dva vlákna současně dekrementovala ob_refcnt jednoho objektu: obě by viděla hodnotu 1, obě by uvolnila paměť – segfault.
Klíčové termíny:
- Segfault: přístup k uvolněné paměti, signál SIGSEGV.
- Opcode: atomární instrukce bajtkódu (dis pro zobrazení).
- Reference counting: ob_refcnt klesne na 0 – objekt je zničen.
- Mutex: pouze jedno vlákno v kritické sekci.
- Race condition: výsledek závisí na pořadí provádění vláken.
- Bytecode: zkompilovaný .py do .pyc, provádí se VM.
- Context switch: režie ~mikrosekundy, kazí CPU-cache.
Příklad race bez GIL:
# thread1: refcount=1 -> free(obj)
# thread2: refcount=1 -> free(obj) -> segfault
Účel GIL v CPythonu
GIL je globální mutex, který umožňuje provádění bajtkódu pouze jednomu vláknu současně. Vlákna mohou pracovat paralelně v systémových voláních, ale ne v Python kódu.
Proč je potřeba:
- Ochrana ob_refcnt před současnými změnami.
- Zjednodušení C-rozšíření bez thread-safety.
- Levná thread-safety bez složité synchronizace.
GIL je relevantní pouze pro CPython. Jython/IronPython ho nemají.
Vnitřní mechanismus GIL (od 3.2+)
Nová implementace v Python/ceval_gil.c: struktura _gil_runtime_state.
Klíčové komponenty:
locked: atomární příznak (0/1).mutex+cond_var: POSIX-primitivy pro čekání.eval_breaker: kontrola po každém opcode.gil_drop_request: požadavek od čekajícího vlákna.
Algoritmus:
- Vlákno drží GIL, provádí bajtkód.
- Po 5 ms (sys.getswitchinterval()) signalizuje jiné vlákno.
- Současné vidí příznak přes eval_breaker, uvolní GIL.
- Čekající zachytí, současné čeká na cond_var.
sys.setswitchinterval(): výchozí 0.005 s. Zvýšit na 0.05–0.1 s pro CPU-bound s NumPy (méně thrashing). Neměnit pro I/O.
Proč Lock/RLock jsou potřeba i s GIL
GIL je atomární pro jeden opcode, ale counter += 1 – to je LOAD_GLOBAL + BINARY_ADD + STORE_GLOBAL.
counter += 1
# Může být přerušeno po LOAD_GLOBAL
# Dvě vlákna: +1 místo +2
Řešení:
import threading
counter = 0
lock = threading.Lock()
def safe_increment():
global counter
with lock:
counter += 1
RLock pro reentrantnost:
class SafeTree:
def __init__(self):
self._lock = threading.RLock()
def insert(self, value):
with self._lock:
self._rebalance() # žádný deadlock
def _rebalance(self):
with self._lock:
pass
Pravidlo: GIL chrání interpret, Lock – obchodní logiku.
CPU-bound vs I/O-bound úlohy
I/O-bound (síť/disk): GIL se uvolňuje v systémových voláních (read/write). threading/asyncio poskytují paralelismus.
import threading, requests
def fetch(url):
r = requests.get(url) # GIL free
return r.status_code
CPU-bound (výpočty): GIL blokuje paralelismus. Použijte multiprocessing.
from multiprocessing import Pool
def heavy_compute(n):
return sum(i * i for i in range(n))
with Pool(4) as p:
results = p.map(heavy_compute, [10**7]*4) # 4 GIL
Uvolnění GIL v C-rozšířeních
NumPy/pandas uvolňují GIL v těžkých operacích:
Py_BEGIN_ALLOW_THREADS
matrix_multiply(a, b, result, n);
Py_END_ALLOW_THREADS
threading + NumPy = skutečný paralelismus. Čistý Python ve vláknech – ne.
asyncio a nezávislost na GIL
Kooperativní multitasking v jednom vlákně. Přepnutí na await.
import asyncio
import aiohttp
async def fetch_data(session, url):
async with session.get(url) as resp:
return await resp.json()
async def main():
async with aiohttp.ClientSession() as session:
tasks = [fetch_data(session, url) for url in urls]
results = await asyncio.gather(*tasks)
threading vs asyncio pro I/O:
- threading: OS-vlákna, režie, race conditions.
- asyncio: 1 vlákno, žádná režie, explicitní řízení.
Úrovně thread/process/async safety
| Typ | Problém | Sdílená paměť | Nástroje | Použití |
|-----|----------|--------------|-------------|------------|
| Thread | race ve vláknech | ano | Lock, Queue, local() | ThreadPoolExecutor |
| Process | závody procesů | explicitně | multiprocessing.Lock, Manager | Celery, Gunicorn |
| Async | závody korutin | jedno vlákno | asyncio.Lock, bez mutability | FastAPI |
Příklad Async race:
# Nebezpečné
async def transfer(from_acc, to_acc, amount):
balance = await db.get_balance(from_acc)
if balance >= amount: # zde yield
await db.deduct(from_acc, amount)
Použijte asyncio.Lock nebo DB-transakce.
Co je důležité
- GIL chrání refcount, ale ne obchodní logiku – vždy používejte Lock.
- Pro CPU-bound: multiprocessing, ne threading.
- C-rozšíření (NumPy) poskytují paralelismus ve vláknech.
- asyncio ignoruje GIL: kooperativnost v jednom vlákně.
- sys.setswitchinterval() je zřídka potřeba, jen pro NumPy-heavy úlohy.
— Editorial Team
Zatím žádné komentáře.