Este código crea una condición de carrera:
import threading ITERS = 100000 x = [0] def worker(): for _ in range(ITERS): x[0] += 1 # this line creates a race condition # because it takes a value, increments and then writes # some inrcements can be done together, and lost def main(): x[0] = 0 # you may use `global x` instead of this list trick too t1 = threading.Thread(target=worker) t2 = threading.Thread(target=worker) t1.start() t2.start() t1.join() t2.join() for i in range(5): main() print(f'iteration {i}. expected x = {ITERS*2}, got {x[0]}')Producción:
$ python3 test.py iteration 0. expected x = 200000, got 200000 iteration 1. expected x = 200000, got 148115 iteration 2. expected x = 200000, got 155071 iteration 3. expected x = 200000, got 200000 iteration 4. expected x = 200000, got 200000Versión Python3:
Python 3.9.7 (default, Sep 10 2021, 14:59:43) [GCC 11.2.0] on linuxPensé que GIL lo evitaría y no permitiría que dos subprocesos se ejecutaran juntos hasta que hicieran algo relacionado con io o llamaran a una biblioteca C. Al menos esto es lo que puede concluir de los documentos .
Entonces, ¿qué hace realmente GIL y cuándo se ejecutan los subprocesos en paralelo?
Leyendo mejor los documentos , creo que ahí está la respuesta:
El mecanismo utilizado por el intérprete de CPython para garantizar que solo un subproceso ejecute el código de bytes de Python a la vez . Esto simplifica la implementación de CPython al hacer que el modelo de objetos (incluidos los tipos integrados críticos como dict) sea implícitamente seguro contra el acceso simultáneo. Bloquear todo el intérprete facilita que el intérprete sea multiproceso, a expensas de gran parte del paralelismo que ofrecen las máquinas multiprocesador.
Sin embargo, algunos módulos de extensión, ya sean estándar o de terceros, están diseñados para liberar la GIL cuando se realizan tareas de computación intensiva, como compresión o hash. Además, el GIL siempre se libera al realizar E/S.
No conozco los aspectos internos, pero supongo que cada línea o bloque de este código de bytes se ejecuta solo, y otros subprocesos están esperando (lo que lo hace lento). Pero algunas líneas constan de varios bloques y no son atómicas.
Esto es lo que obtienes si ejecutas dis.dis('x[0] += 1') :
0 LOAD_NAME 0 (x) 2 LOAD_CONST 0 (0) 4 DUP_TOP_TWO 6 BINARY_SUBSCR 8 LOAD_CONST 1 (1) 10 INPLACE_ADD 12 ROT_THREE 14 STORE_SUBSCR 16 LOAD_CONST 2 (None) 18 RETURN_VALUE Algunos de estos se ejecutan de manera concurrente y hacen la condición de carrera. Entonces, GIL solo garantiza que las partes internas de estructuras como list o dict no se dañarán.