Tengo una aplicación que está adquiriendo un bloqueo en un bucle en un hilo para realizar alguna tarea. También hay un segundo hilo que también quiere adquirir el bloqueo de vez en cuando. El problema es que este segundo hilo apenas tiene la oportunidad de realizar su trabajo, ya que el primero casi siempre se bloquea primero. Espero que el siguiente código aclare lo que estoy tratando de decir:

import time
from threading import Lock, Thread

lock = Lock()


def loop():
    while True:
        with lock:
            time.sleep(0.1)

thread = Thread(target=loop)
thread.start()

before = time.time()
lock.acquire()
print('Took {}'.format(time.time() - before))

Si la aplicación llega a print, notará que necesitaba mucho más de solo 0.1s. Pero a veces también sucede que solo espera indefinidamente. He probado esto tanto en Python 2.7.11 como en Python 3.4.3 en Debian Linux 8 y funciona de manera idéntica.

Este comportamiento es contra-intuitivo para mí. Después de todo cuando se libera el bloqueo en loop, el lock.acquire ya estaba esperando su lanzamiento y debería adquirir de inmediato la cerradura. Pero en cambio parece que el bucle llega a adquirir la cerradura primero, a pesar de que no estaba esperando su liberación en absoluto en el momento de lanzamiento.

La solución que he encontrado es dormir entre cada iteración del bucle en estado desbloqueado, pero eso no me parece una solución elegante, ni me explica qué está sucediendo.

¿Qué me estoy perdiendo?

11
Jakub 9 may. 2016 a las 22:01

3 respuestas

La mejor respuesta

Parece que esto se debe a la programación de subprocesos del sistema operativo. Supongo que cualquiera de los dos sistemas operativos da prioridad a los subprocesos intensivos de CPU (lo que sea que eso signifique) o elegir un siguiente subproceso para adquirir el bloqueo (hecho por el sistema operativo) lleva más tiempo que adquirir el bloqueo El segundo hilo. De cualquier manera, no se puede deducir mucho sin conocer los aspectos internos del sistema operativo.

Pero no es GIL desde este código:

#include <mutex>
#include <iostream>
#include <chrono>
#include <thread>

std::mutex mutex;

void my_thread() {
    int counter = 100;
    while (counter--) {
        std::lock_guard<std::mutex> lg(mutex);
        std::this_thread::sleep_for(std::chrono::milliseconds(500));
        std::cout << "." << std::flush;
    }
}

int main (int argc, char *argv[]) {
    std::thread t1(my_thread);
    auto start = std::chrono::system_clock::now();
    // added sleep to ensure that the other thread locks lock first
    std::this_thread::sleep_for(std::chrono::milliseconds(1000));
    {
        std::lock_guard<std::mutex> lg(mutex);
        auto end = std::chrono::system_clock::now();
        auto diff = end - start;
        std::cout << "Took me " << diff.count() << std::endl;
    }
    t1.join();
    return 0;
};

Que es solo una versión C ++ 11 de su código, da exactamente el mismo resultado (probado en Ubuntu 16.04).

6
freakish 10 may. 2016 a las 07:52

Multithreading en CPython es algo complicado. Para facilitar la implementación (de la gestión de la memoria, entre otras cosas), CPython tiene un "Bloqueo de intérprete global" incorporado. Este bloqueo garantiza que solo un subproceso a la vez pueda ejecutar el código de bytes de Python.

Un hilo liberará el GIL cuando haga E / S o llegue a una extensión C. Y si no lo hace, el GIL se extraerá de él a ciertos intervalos. Entonces, si un hilo está ocupado girando como lo está tu hilo, en un momento se verá obligado a abandonar el GIL. Y esperaría que en ese caso otro hilo tenga la oportunidad de ejecutarse. Pero debido a que los subprocesos de Python son básicamente subprocesos del sistema operativo, el sistema operativo también tiene algo que decir en la programación. Y allí un hilo que está constantemente ocupado puede tener una mayor prioridad y, por lo tanto, tener más oportunidades de ejecutarse.

Para una mirada más profunda, mira el video entendiendo el Python GIL por David Beazley.

2
Roland Smith 9 may. 2016 a las 21:03

Para agregar a la útil respuesta de @ freakish, para mí la solución práctica fue agregar un pequeño sueño justo antes de que el hilo codicioso adquiera el bloqueo. En tu caso:

def loop():
    while True:
        # give time for competing threads to acquire lock
        time.sleep(0.001)
        with lock:
            time.sleep(0.1)

Dormir durante 0 segundos (como se sugiere en los comentarios) no funcionó para mí.

0
101 27 may. 2019 a las 01:55