2011-06-18 8 views
5

He leído muchos ejemplos sobre el bloqueo de hilos ... pero, ¿por qué debería bloquearlos? Según entiendo, cuando inicia hilos sin unirlos, competirán con el hilo principal y todos los demás hilos por recursos y luego los ejecutarán, a veces simultáneamente, otras veces no.¿Por qué deberías bloquear los hilos?

¿El bloqueo asegura que los hilos NO se ejecutan simultáneamente?

Además, ¿qué sucede con los subprocesos que se ejecutan simultáneamente? ¿No es eso aún mejor? (ejecución general más rápida)

Cuando bloquea los hilos, ¿los bloqueará a todos o puede elegir cuáles desea bloquear? (Lo que realmente hace de bloqueo ...)

Me refiero a la utilización de las funciones de bloqueo como el bloqueo() y adquirir en el módulo threading por cierto ...

+0

No, no estoy preguntando sobre el GIL, conozco las limitaciones del mismo en python y estoy contento con él, la cuestión es bloquear hilos con acquire() y release() no relacionados con el GIL (aparte de tiene un candado en el nombre) – MistahX

+1

Reetiquetado, como no exclusivo de 'python' en absoluto. –

+0

reticulado como python, me refiero a los métodos de bloqueo en el módulo de subprocesamiento, se olvidó de agregar eso en – MistahX

Respuesta

11

Una cerradura permite forzar el acceso a múltiples hilos un recurso a la vez, en lugar de que todos ellos intenten acceder al recurso simultáneamente.

Como observa, normalmente desea que los hilos se ejecuten simultáneamente. Sin embargo, imagine que tiene dos hilos y ambos escriben en el mismo archivo. Si intentan escribir en el mismo archivo al mismo tiempo, su salida se mezclará y ninguno de los hilos logrará poner en el archivo lo que quisieran.

Ahora quizás este problema no aparezca todo el tiempo. La mayoría de las veces, los hilos no intentarán escribir en el archivo todo a la vez. Pero a veces, tal vez una vez en mil carreras, lo hacen. Así que tal vez tengas un error que ocurre aparentemente al azar y es difícil de reproducir y, por lo tanto, difícil de solucionar. ¡Uf!

O tal vez ... y esto ha sucedido en la empresa para la que trabajo ... tiene esos errores pero no sabe que están ahí porque casi ninguno de sus clientes tiene más de 4 CPU. Entonces todos comienzan a comprar cajas de 16 CPU ... y su software ejecuta tantos hilos como núcleos de CPU, por lo que ahora hay 4 veces más hilos y de repente se cuelga mucho o se obtienen resultados incorrectos.

De todos modos, de vuelta al archivo. Para evitar que los hilos se pisen entre sí, cada hilo debe adquirir un bloqueo en el archivo antes de escribir en él. Solo un hilo puede mantener el bloqueo a la vez, por lo que solo un hilo puede escribir en el archivo a la vez. El hilo mantiene el bloqueo hasta que se termina de escribir en el archivo, luego libera el bloqueo para que otro hilo pueda usar el archivo.

Si los hilos están escribiendo en archivos diferentes, este problema nunca se presenta. Esa es una solución: haga que sus hilos escriban en diferentes archivos y combínelos después si es necesario. Pero esto no siempre es posible; a veces, solo hay uno de algo.

No tiene que ser archivos. Supongamos que está intentando simplemente contar el número de apariciones de la letra "A" en un grupo de archivos diferentes, un hilo por archivo. Piensas, bueno, obviamente, haré que todos los hilos incrementen la misma ubicación de memoria cada vez que vean una "A". ¡Pero! Cuando vaya a incrementar la variable que mantiene el recuento, la computadora lee la variable en un registro, incrementa el registro y luego almacena el valor de nuevo. ¿Qué sucede si dos hilos leen el valor al mismo tiempo, lo incrementan al mismo tiempo y lo almacenan al mismo tiempo? Ambos comienzan en, digamos, 10, lo incrementan a 11, almacenan 11 de nuevo. Entonces el contador es 11 cuando debería ser 12: has perdido un conteo.

La adquisición de bloqueos puede ser costosa, ya que debe esperar hasta que alguien más esté utilizando el recurso. Esta es la razón por la cual el bloqueo de Intérprete global de Python es un cuello de botella de rendimiento.Por lo tanto, puede decidir evitar el uso de recursos compartidos en absoluto. En lugar de usar una sola ubicación de memoria para mantener el número de "A" en sus archivos, cada hilo mantiene su propio recuento, y usted los agrega todos al final (similar a la solución que sugerí con los archivos, curiosamente) .

+0

Está bien, esto tiene sentido, pero los bloqueos estoy hablando de bloquear los hilos, no los archivos? Entonces, ¿qué hacen? – MistahX

+1

@MistahX: No, bloquean lo que sea que decidas bloquear. Los usa como mejor le parezca para evitar que varios hilos hagan lo mismo al mismo tiempo. Son primitivos, construyes a partir de ellos lo que necesitas. Si ajusta el acceso a un archivo en un bloqueo, efectivamente está "bloqueando" ese archivo. –

+1

Eso está bloqueando un recurso, no el hilo real. – Alan

9

En primer lugar, los bloqueos están diseñados para proteger los recursos; los hilos no están 'bloqueados' o 'desbloqueados' ellos/adquirir/un bloqueo (en un recurso) y/liberar/un bloqueo (en un recurso).

Tiene razón en que desea hilos se ejecuten simultáneamente tanto como sea posible, pero vamos a echar un vistazo a esto:

y=10 

def doStuff(x): 
    global y 
    a = 2 * y 
    b = y/5 
    y = a + b + x 
    print y 

t1 = threading.Thread(target=doStuff, args=(8,)) 
t2 = threading.Thread(target=doStuff, args=(8,)) 
t1.start() 
t2.start() 
t1.join() 
t2.join() 

Ahora, es posible saber que ninguno de estos temas podría completar e imprimir primero . Esperaría ver tanto la salida 30.

Pero es posible que no.

y es un recurso compartido, y en este caso, los bits que leen y escriben a y son parte de lo que se llama una "sección crítica" y deberían estar protegidos por un bloqueo. La razón es que no obtienes unidades de trabajo: cualquiera de los hilos puede obtener CPU en cualquier momento.

Piense en ello como esto:

t1 está ejecutando felizmente código y se realiza

a = 2 * y 

Ahora T1 tiene a = 20 y deja de ejecutarse durante un tiempo. t2 se activa mientras t1 espera más tiempo de CPU. t2 ejecuta:

a = 2 * y 
b = y/5 
y = a + b + x 

en este punto la variable global y = 30

t2 detiene paradas por un poco y t1 recoge de nuevo. se ejecuta:

b = y/5 
y = a + b + x 

Puesto que y era 30 cuando se estableció b, b = 6 e y se establece ahora en 34.

el orden de las impresiones no es determinista, así y es posible obtener la 30 primero o el 34 primero.

usando una cerradura tendríamos:

global l 
l = threading.Lock() 
def doStuff(x): 
    global y 
    global l 
    l.acquire() 
    a = 2 * y 
    b = y/5 
    y = a + b + x 
    print y 
    l.release() 

Esto necesariamente hace que esta sección del código lineal - sólo un hilo a la vez. Pero si todo tu programa es secuencial, no deberías estar usando subprocesos de todos modos. La idea es que ganes velocidad en función del porcentaje de código que tienes, que puede ejecutar bloqueos externos y ejecutarse en paralelo. Esta es (una de las razones) por la que el uso de subprocesos en un sistema de 2 núcleos no duplica el rendimiento para todo.

el propio bloqueo también es un recurso compartido, pero debe ser: una vez que un hilo adquiere el bloqueo, todos los otros hilos que intenten adquirir el/mismo/bloqueo se bloquearán hasta que se libere. Una vez que se libera, el primer hilo para avanzar y adquirir el bloqueo bloqueará todos los demás hilos de espera.

¡Espero que eso sea suficiente para continuar!

Cuestiones relacionadas