2009-11-11 32 views
35

Quiero crear un fragmento de código que no sea seguro para subprocesos para la experimentación, y esas son las funciones a las que van a llamar los 2 hilos.¿El operador + = es seguro para subprocesos en Python?

c = 0 

def increment(): 
    c += 1 

def decrement(): 
    c -= 1 

¿Es seguro este hilo código?

Si no es así, ¿puedo entender por qué no se hilo de seguridad, y qué tipo de declaraciones suele dar lugar a operaciones no compatibles con el proceso.

Si es seguro para subprocesos, ¿cómo puedo hacerlo explícitamente no seguro para subprocesos?

+3

Debe haber una desaceleración 'c' mundial al inicio de cada función o esto realmente no hacer nada. – JoshB

Respuesta

5

códigos de operación individuales son hilos de proceso seguro debido a la GIL pero nada más:

import time 
class something(object): 
    def __init__(self,c): 
     self.c=c 
    def inc(self): 
     new = self.c+1 
     # if the thread is interrupted by another inc() call its result is wrong 
     time.sleep(0.001) # sleep makes the os continue another thread 
     self.c = new 


x = something(0) 
import threading 

for _ in range(10000): 
    threading.Thread(target=x.inc).start() 

print x.C# ~900 here, instead of 10000 

Cada recurso compartido por múltiples hilos debe tener una cerradura.

+2

Esto no responde a la pregunta, que es aproximadamente + '' = –

+0

también, y me corrija si estoy equivocado, 'x.c' de impresión no espera a los hilos para terminar. Así que la mayoría de ellos todavía se están ejecutando cuando imprime la salida. –

4

Respuesta corta: no.

Respuesta larga: generalmente no.

Mientras GIL de CPython hace que los códigos de operación individuales thread-safe, esto no es el comportamiento general. No puede suponer que incluso operaciones simples como una adición es una instrucción atómica. La adición solo puede estar a la mitad cuando se ejecuta otro hilo.

Y tan pronto como sus funciones accedan a una variable en más de un código de operación, la seguridad de su hilo habrá desaparecido. Puede generar seguridad de subprocesos, si envuelve sus cuerpos de función en locks. Pero tenga en cuenta que los bloqueos pueden ser computacionalmente costosos y pueden generar interbloqueos.

+0

Buena respuesta. Gracias. ¿Puedo saber cuáles son los códigos de operación individuales? – nubela

+0

Todo tiene múltiples códigos de operación/es un compuesto a menos que se indique lo contrario. – ebo

+1

Tanto la respuesta corta como la larga son no. –

27

(nota: se necesitaría global c en cada función para hacer su trabajo de código.)

¿Es este hilo código de seguridad?

No. Sólo una sola instrucción de código de bytes es 'atómica' en CPython, y una += puede no resultar en un único código de operación, incluso cuando los valores implicados son números enteros simples:

>>> c= 0 
>>> def inc(): 
...  global c 
...  c+= 1 

>>> import dis 
>>> dis.dis(inc) 

    3   0 LOAD_GLOBAL    0 (c) 
       3 LOAD_CONST    1 (1) 
       6 INPLACE_ADD   
       7 STORE_GLOBAL    0 (c) 
      10 LOAD_CONST    0 (None) 
      13 RETURN_VALUE   

Así que un hilo podría llegar al índice 6 con c y 1 cargado, renunciar al GIL y dejar que otro hilo entre, lo que ejecuta un inc y duerme, devolviendo el GIL al primer hilo, que ahora tiene el valor incorrecto.

En cualquier caso, lo atómico es un detalle de implementación en el que no debe confiar. Los códigos de bytes pueden cambiar en versiones futuras de CPython, y los resultados serán totalmente diferentes en otras implementaciones de Python que no se basan en un GIL. Si necesita seguridad de hilo, necesita un mecanismo de bloqueo.

0

¿Está seguro de que las funciones incrementan y disminuyen la ejecución sin ningún error?

creo que debería lanzar un UnboundLocalError porque hay que indicar explícitamente Python que desea utilizar la variable global llamada 'c'.

Así que cambia la subasta (también disminuir) a la siguiente:

def increment(): 
    global c 
    c += 1 

creo que es su código es el hilo inseguro. This article acerca de los mecanismos de sincronización de subprocesos en Python puede ser útil.

2

Si realmente quieren hacer que el código no flujos seguros, y tienen buenas posibilidades de "malo" cosas sucediendo realmente sin ti tratando como diez mil veces (o una vez al verdadero No quieren "malo" cosas suceda), puede 'jitter' con su código duerme explícitas:

def íncrement(): 
    global c 
    x = c 
    from time import sleep 
    sleep(0.1) 
    c = x + 1 
7

es fácil demostrar que su código es no seguro para subprocesos. Puede aumentar la probabilidad de ver la condición de carrera usando un descanso en las partes críticas (esto simplemente simula una CPU lenta). Sin embargo, si ejecuta el código durante el tiempo suficiente, debería ver la condición de carrera con el tiempo.

from time import sleep 
c = 0 

def increment(): 
    global c 
    c_ = c 
    sleep(0.1) 
    c = c_ + 1 

def decrement(): 
    global c 
    c_ = c 
    sleep(0.1) 
    c = c_ - 1 
+0

Usar dormir para este tipo de cosas es muy incorrecto. ¿Cómo se te ocurrió el valor 0.1? ¿un procesador más rápido necesitaría dormir más? Usar el sueño para resolver problemas casi siempre es incorrecto. – omribahumi

+5

@omribahumi, ¿qué? Creo que estás confundido por el propósito de mi respuesta. Este código es un _example_ de lo fácil que es _probar_ que una pieza particular de código no sea segura para subprocesos. El sueño simplemente está allí como un marcador de posición para estimular el procesamiento adicional que normalmente estaría allí. Si te refieres a que usar el sueño es la forma incorrecta de evitar las condiciones de carrera, ciertamente estoy de acuerdo, pero eso no es lo que dice mi respuesta. –

+0

Lo siento, mi mal.Lo leí "disminuir" por error. – omribahumi

64

No, este código es absolutamente, demostrablemente no inseguro.

import threading 

i = 0 

def test(): 
    global i 
    for x in range(100000): 
     i += 1 

threads = [threading.Thread(target=test) for t in range(10)] 
for t in threads: 
    t.start() 

for t in threads: 
    t.join() 

assert i == 1000000, i 

falla constantemente.

i + = 1 tiene por cuatro códigos de operación: carga i, la carga 1, añadir los dos, y almacenar de nuevo a i. El intérprete de Python conmuta los hilos activos (liberando el GIL de un hilo para que otro hilo pueda tenerlo) cada 100 códigos de operación. (Ambos son detalles de implementación). La condición de carrera ocurre cuando la preferencia de 100 opcode ocurre entre la carga y el almacenamiento, permitiendo que otro subproceso comience a incrementar el contador. Cuando vuelve al hilo suspendido, continúa con el valor anterior de "i" y mientras tanto deshace los incrementos ejecutados por otros hilos.

Por lo que es multi-hilo es sencillo; añadir un bloqueo:

#!/usr/bin/python 
import threading 
i = 0 
i_lock = threading.Lock() 

def test(): 
    global i 
    i_lock.acquire() 
    try: 
     for x in range(100000): 
      i += 1 
    finally: 
     i_lock.release() 

threads = [threading.Thread(target=test) for t in range(10)] 
for t in threads: 
    t.start() 

for t in threads: 
    t.join() 

assert i == 1000000, i 
+8

Mucho más útil que la respuesta aceptada. ¡Gracias! – RoboCop87

+5

debería ser la respuesta aceptada. – laike9m

+2

Up-votado. Su ejemplo de bloqueo sería más ilustrativo si el bloqueo fuera adquirido y liberado para cada incremento en lugar de cada incremento de 100.000. ¿Por qué molestarse con los hilos si se van a ejecutar de forma secuencial sin superposición alguna? – MarredCheese

14

para estar seguro que recomiendo usar una cerradura:

import threading 

class ThreadSafeCounter(): 
    def __init__(self): 
     self.lock = threading.Lock() 
     self.counter=0 

    def increment(self): 
     with self.lock: 
      self.counter+=1 


    def decrement(self): 
     with self.lock: 
      self.counter-=1 

El decorador sincronizada también puede ayudar a mantener el código fácil de leer.

Cuestiones relacionadas