Soporte de hilos de bajo nivel

sys incluye funciones de bajo nivel para controlar y depurar el comportamiento del hilo.

Intervalo de cambio

Python 3 usa un bloqueo global para evitar que hilos separados corrompan el estado del intérprete. Después de un intervalo de tiempo configurable, la ejecución del código de bytes se detiene y el intérprete verifica si es necesario ejecutar algún controlador de señal. Durante la misma verificación, el bloqueo actual del intérprete (GIL) también se libera por el hilo actual y luego se vuelve a adquirir, y se da prioridad a otros hilos sobre el hilo que acaba de liberar el bloqueo.

El intervalo de cambio predeterminado es de 5 milisegundos y el valor actual siempre se puede recuperar con sys.getswitchinterval(). Cambiar el intervalo con sys.setswitchinterval() puede tener un impacto en el rendimiento de una aplicación, dependiendo de la naturaleza de las operaciones que se realizan.

sys_switchinterval.py
import sys
import threading
from queue import Queue


def show_thread(q):
    for i in range(5):
        for j in range(1000000):
            pass
        q.put(threading.current_thread().name)
    return


def run_threads():
    interval = sys.getswitchinterval()
    print('interval = {:0.3f}'.format(interval))
    q = Queue()
    threads = [
        threading.Thread(target=show_thread,
                         name='T{}'.format(i),
                         args=(q,))
        for i in range(3)
    ]
    for t in threads:
        t.setDaemon(True)
        t.start()
    for t in threads:
        t.join()
    while not q.empty():
        print(q.get(), end=' ')
    print()
    return


for interval in [0.001, 0.1]:
    sys.setswitchinterval(interval)
    run_threads()
    print()

Cuando el intervalo de cambio es menor que la cantidad de tiempo que tarda un subproceso en ejecutarse hasta su finalización, el intérprete otorga otro control de subproceso para que se ejecute durante un tiempo. Esto se ilustra en el primer conjunto de salida donde el intervalo se establece en 1 milisegundo.

Para intervalos más largos, el subproceso activo podrá completar más trabajo antes de verse obligado a liberar el control. Esto se ilustra por el orden de los valores de nombre en la cola en el segundo ejemplo usando un intervalo de 10 milisegundos.

$ python3 sys_switchinterval.py

interval = 0.001
T0 T1 T2 T1 T0 T2 T0 T1 T2 T1 T0 T2 T1 T0 T2

interval = 0.100
T0 T0 T0 T0 T0 T1 T1 T1 T1 T1 T2 T2 T2 T2 T2

Muchos factores además del intervalo de cambio pueden controlar el comportamiento de cambio de contexto de los hilos de Python. Por ejemplo, cuando un subproceso realiza E/S, libera el GIL y, por lo tanto, puede permitir que otro subproceso se haga cargo de la ejecución.

Depuración

La identificación de puntos muertos puede ser uno de los aspectos más difíciles de trabajar con subprocesos. sys._current_frames() puede ayudar al mostrar exactamente dónde se detiene un hilo.

sys_current_frames.py
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
import sys
import threading
import time

io_lock = threading.Lock()
blocker = threading.Lock()


def block(i):
    t = threading.current_thread()
    with io_lock:
        print('{} with ident {} going to sleep'.format(
            t.name, t.ident))
    if i:
        blocker.acquire()  # acquired but never released
        time.sleep(0.2)
    with io_lock:
        print(t.name, 'finishing')
    return


# Create and start several threads that "block"
threads = [
    threading.Thread(target=block, args=(i,))
    for i in range(3)
]
for t in threads:
    t.setDaemon(True)
    t.start()

# Map the threads from their identifier to the thread object
threads_by_ident = dict((t.ident, t) for t in threads)

# Show where each thread is "blocked"
time.sleep(0.01)
with io_lock:
    for ident, frame in sys._current_frames().items():
        t = threads_by_ident.get(ident)
        if not t:
            # Main thread
            continue
        print('{} stopped in {} at line {} of {}'.format(
            t.name, frame.f_code.co_name,
            frame.f_lineno, frame.f_code.co_filename))

El diccionario devuelto por sys._current_frames() está tecleado en el identificador de hilo, en lugar de su nombre. Se necesita un poco de trabajo para asignar esos identificadores nuevamente al objeto de hilo.

Como Thread-1 no duerme, termina antes de que se verifique su estado. Como ya no está activo, no aparece en la salida. Thread-2 adquiere el bloqueo blocker, luego duerme por un corto período. Mientras tanto Thread-3 intenta adquirir blocker pero no puede porque Thread-2 ya lo tiene.

$ python3 sys_current_frames.py

Thread-1 with ident 123145307557888 going to sleep
Thread-1 finishing
Thread-2 with ident 123145307557888 going to sleep
Thread-3 with ident 123145312813056 going to sleep
Thread-3 stopped in block at line 18 of sys_current_frames.py
Thread-2 stopped in block at line 19 of sys_current_frames.py

Ver también

  • threading – El módulo threading incluye clases para crear hilos Python.
  • Queue – El módulo Queue proporciona una implementación segura de subprocesos de la estructura de datos FIFO.
  • Reworking the GIL – Correo electrónico de Antoine Pitrou a la lista de correo python-dev que describe los cambios en la implementación de GIL para introducir el intervalo de cambio.