2010-04-29 20 views
5

Algunas discusiones en otra pregunta me han alentado a comprender mejor los casos en los que se requiere el bloqueo en programas de Python multiproceso.¿Hay algún caso en el que los subprocesos de Python puedan manipular de forma segura el estado compartido?

por this artículo sobre enhebrar en Python, tengo varios ejemplos sólidos y comprobables de errores que pueden ocurrir cuando varios hilos acceden al estado compartido. El ejemplo de condición de carrera proporcionado en esta página implica carreras entre hilos que leen y manipulan una variable compartida almacenada en un diccionario. Creo que el caso de una carrera aquí es muy obvio, y afortunadamente es eminentemente comprobable.

Sin embargo, no he podido evocar una condición de carrera con operaciones atómicas tales como listas añadidas o incrementos variables. Esta prueba intenta exhaustivamente para demostrar una carrera tan:

from threading import Thread, Lock 
import operator 

def contains_all_ints(l, n): 
    l.sort() 
    for i in xrange(0, n): 
     if l[i] != i: 
      return False 
    return True 

def test(ntests): 
    results = [] 
    threads = [] 
    def lockless_append(i): 
     results.append(i) 
    for i in xrange(0, ntests): 
     threads.append(Thread(target=lockless_append, args=(i,))) 
     threads[i].start() 
    for i in xrange(0, ntests): 
     threads[i].join() 
    if len(results) != ntests or not contains_all_ints(results, ntests): 
     return False 
    else: 
     return True 

for i in range(0,100): 
    if test(100000): 
     print "OK", i 
    else: 
     print "appending to a list without locks *is* unsafe" 
     exit() 

He corrido la prueba anterior sin fallo (100x 100k APPENDs multihilo). ¿Alguien puede hacer que falle? ¿Hay alguna otra clase de objeto que se pueda comportar incorrectamente a través de una modificación atómica, incremental por hilos?

¿Estas semánticas implícitamente 'atómicas' se aplican a otras operaciones en Python? ¿Está esto directamente relacionado con el GIL?

+0

Las pruebas no son un método válido para probar la corrección en aplicaciones concurrentes. Es muy fácil para cualquier prueba en particular intercalar la ejecución de una manera inesperadamente predecible que nunca desencadenaría el problema, sin embargo, el más pequeño de los cambios en el código (es decir.al extenderlo a una situación del mundo real) podría mostrar instantáneamente el defecto. La corrección del software concurrente debe ser probada, no probada. – Kylotan

Respuesta

7

Agregar a una lista es seguro para subprocesos, sí. Solo se puede agregar a una lista mientras se mantiene el GIL, y la lista se cuida de no liberar el GIL durante la operación append (que es, después de todo, una operación bastante simple). El orden en el que las operaciones de adición de diferentes hilos van a través de, por supuesto, está en juego, pero todas serán estrictamente operaciones serializadas porque el GIL nunca se lanza durante un apéndice.

Lo mismo no es necesariamente cierto para otras operaciones. Muchas operaciones en Python pueden ocasionar que se ejecute código Python arbitrario, lo que a su vez puede hacer que se libere el GIL. Por ejemplo, i += 1 son tres operaciones distintas, "obtenga i", "agréguelo 1" y "almacénelo en i". "Agregar 1 a él" se traduciría (en este caso) en it.__iadd__(1), lo que puede activarse y desaparecer.

Los objetos Python en sí guardan su propio estado interno: los dictados no se corrompen con dos subprocesos diferentes que intentan establecer elementos en ellos. Pero si se supone que los datos en el dictado son internamente consistentes, tampoco el dict ni el GIL hacen nada para proteger eso, excepto (en la moda habitual) al hacer que sea menos probable pero posible las cosas terminan siendo diferentes de lo que creías.

1

En CPython, interruptor de hilo se hace cuando sys.getcheckinteval() bycodes se han ejecutado. Por lo tanto, un cambio de contexto nunca puede ocurrir durante la ejecución de un solo bytecode, y las operaciones codificadas como bytecode son intrínsecamente atómicas y enhebradas, a menos que bytecode ejecute otro código Python o llame al código C que libera el GIL. La mayoría de las operaciones en los tipos de colección incorporados (dict, list, etc.) entran en la categoría 'inherentemente enhebrable'.

Sin embargo, este es un detalle de implementación que es específico de la implementación C de Python, y no se debe confiar en él. Es posible que otras versiones de Python (Jython, IronPython, PyPy, etc.) no se comporten de la misma manera. Tampoco hay garantía de que las versiones futuras de CPython mantendrán este comportamiento.

+0

Esto siguió mi intuición sobre lo que estaba sucediendo, pero nunca pensé en examinar las otras implementaciones. Solo estoy usando CPython. Considero que, para que el código sea a prueba del futuro frente a implementaciones alternativas, no se debe confiar en este detalle. –

+0

La distinción bytecode generalmente no es muy interesante, porque casi todos los bytecodes tienen el potencial de ejecutar más código Python, y algunos que no tienen el potencial de liberar el GIL ellos mismos. 'list.append()', por ejemplo, no es un bytecode, y el trabajo 'append' real se ejecuta con el código de operación' CALL_FUNCTION', que es * muy probable * que ejecute más código :-) –

Cuestiones relacionadas