una tuplas por listas lista elementos diccionarios conjuntos comprensión colecciones agregar python multithreading thread-safety python-multithreading

tuplas - listas python



¿Se garantiza que la extensión de una lista de Python(por ejemplo, l+=[1]) sea segura para subprocesos? (2)

Si tengo un entero i , no es seguro hacer i += 1 en varios subprocesos:

>>> i = 0 >>> def increment_i(): ... global i ... for j in range(1000): i += 1 ... >>> threads = [threading.Thread(target=increment_i) for j in range(10)] >>> for thread in threads: thread.start() ... >>> for thread in threads: thread.join() ... >>> i 4858 # Not 10000

Sin embargo, si tengo una lista l , parece seguro hacer l += [1] en varios subprocesos:

>>> l = [] >>> def extend_l(): ... global l ... for j in range(1000): l += [1] ... >>> threads = [threading.Thread(target=extend_l) for j in range(10)] >>> for thread in threads: thread.start() ... >>> for thread in threads: thread.join() ... >>> len(l) 10000

¿ l += [1] garantiza que l += [1] seguro para subprocesos? Si es así, ¿esto se aplica a todas las implementaciones de Python o solo a CPython?

Edición: parece que l += [1] es seguro para subprocesos pero l = l + [1] no es ...

>>> l = [] >>> def extend_l(): ... global l ... for j in range(1000): l = l + [1] ... >>> threads = [threading.Thread(target=extend_l) for j in range(10)] >>> for thread in threads: thread.start() ... >>> for thread in threads: thread.join() ... >>> len(l) 3305 # Not 10000


De http://effbot.org/pyfaq/what-kinds-of-global-value-mutation-are-thread-safe.htm :

Las operaciones que reemplazan a otros objetos pueden invocar el método __del__ esos otros objetos cuando su conteo de referencia llega a cero, y eso puede afectar las cosas. Esto es especialmente cierto para las actualizaciones masivas de diccionarios y listas.

Las siguientes operaciones son todas atómicas (L, L1, L2 son listas, D, D1, D2 son dictados, x, y son objetos, i, j son ints):

L.append(x) L1.extend(L2) x = L[i] x = L.pop() L1[i:j] = L2 L.sort() x = y x.field = y D[x] = y D1.update(D2) D.keys()

Estos no son

i = i+1 L.append(L[-1]) L[i] = L[j] D[x] = D[x] + 1

Lo anterior es puramente específico de CPython y puede variar según la implementación de Python, como PyPy.

Por cierto, hay un problema abierto para documentar las operaciones atómicas de Python: https://bugs.python.org/issue15339


No hay una respuesta feliz ;-) a esto. No hay nada garantizado acerca de esto, lo que puede confirmar simplemente observando que el manual de referencia de Python no ofrece ninguna garantía sobre la atomicidad.

En CPython es una cuestión de pragmática. Como dice una parte del artículo de effbot,

En teoría, esto significa que una contabilidad exacta requiere un entendimiento exacto de la implementación del código de bytes PVM [Máquina virtual de Python].

Y esa es la verdad. Un experto en CPython sabe que L += [x] es atómico porque conocen todo lo siguiente:

  • += compila en un bytecode INPLACE_ADD .
  • La implementación de INPLACE_ADD para los objetos de la lista se escribe completamente en C (no hay ningún código Python en la ruta de ejecución, por lo que no se puede liberar GIL entre los INPLACE_ADD de INPLACE_ADD ).
  • En listobject.c , la implementación de INPLACE_ADD es la función list_inplace_concat() , y nada durante su ejecución necesita ejecutar ningún código Python de usuario (si lo hizo, el GIL puede ser liberado nuevamente).

Todo esto puede parecer increíblemente difícil de mantener claro, pero para alguien con el conocimiento de effbot sobre los aspectos internos de CPython (en el momento en que escribió ese artículo), realmente no lo es. De hecho, dada esa profundidad de conocimiento, es obvio ;-)

Por lo tanto, como cuestión de pragmática , los expertos de CPython siempre han confiado libremente en que "las operaciones que ''parecen atómicas'' deberían ser realmente atómicas", y que también guiaron algunas decisiones lingüísticas. Por ejemplo, una operación que falta en la lista de effbot (agregada al idioma después de que escribió ese artículo):

x = D.pop(y) # or ... x = D.pop(y, default)

Un argumento (en ese momento) a favor de agregar dict.pop() fue precisamente que la implementación obvia de C sería atómica, mientras que la alternativa en uso (en ese momento):

x = D[y] del D[y]

no fue atómico (la recuperación y la eliminación se realizan a través de distintos bytecodes, por lo que los hilos pueden cambiar entre ellos).

Pero los docs nunca dijeron que .pop() era atómico, y nunca lo hará. Este es un tipo de "adultos que consienten": si eres lo suficientemente experto como para explotar esto a sabiendas, no necesitas ser agarrado. Si no eres lo suficientemente experto, entonces se aplica la última oración del artículo de effbot:

En caso de duda, utilice un mutex!

Como una cuestión de necesidad pragmática, los desarrolladores centrales nunca romperán la atomicidad de los ejemplos de D.pop() o de D.pop() o D.setdefault() ) en CPython. Sin embargo, otras implementaciones no tienen ninguna obligación de imitar estas elecciones pragmáticas. De hecho, dado que la atomicidad en estos casos se basa en la forma específica de código de bytes de CPython combinada con el uso de CPython de un bloqueo de intérprete global que solo puede liberarse entre códigos de bytes, podría ser un dolor real para otras implementaciones imitarlos.

Y nunca se sabe: ¡alguna versión futura de CPython puede eliminar la GIL también! Lo dudo, pero es teóricamente posible. Pero si eso sucede, apuesto a que una versión paralela que retenga la GIL también se mantendrá, porque una gran cantidad de código (especialmente los módulos de extensión escritos en C ) también se basa en la GIL para la seguridad de los hilos.

Vale la pena repetir:

En caso de duda, utilice un mutex!