Bienvenidos sean a este post, hoy veremos como trabajar con las race conditions.
En este post hablamos del problema conocido como race condition y vimos en teoria como solucionarlo por medio de lock pero tambien vimos que puede caer en un deadlock, con algunas posibles soluciones, pero aqui veremos como realmente es esta condicion, para ello vamos a utilizar un ejemplo, primero crearemos un archivo llamado race_condition.py y luego le agregaremos el siguiente codigo:
race_condition.py
import threading
from time import sleep
from random import random
contador = 0
randsleep = lambda: sleep(0.1 * random())
def incr(n):
global contador
t = threading.current_thread()
for contar in range(n):
actual = contador
randsleep()
contador = actual + 1
randsleep()
print(f'{t.name} => {contador}')
n = 5
t1 = threading.Thread(target=incr,name='t1', args=(n, ))
t2 = threading.Thread(target=incr,name='t2', args=(n, ))
t1.start()
t2.start()
t1.join()
t2.join()
print(f'Contador: {contador}')
Primero importaremos algunos modulos que ya hemos visto en otros ejemplos, el primero sera para manejar los threads, el segundo para generar una pausa y el tercero para generar numeros al azar, despues tenemos una variable para guardar un conteo, ya lo veremos, y el otro sera para generar un valor de pausa al azar, utilizamos a lambda para generar la funcion anonima, si quieren saber mas les recomiendo este post, y en ella ira el sleep ajustado por random, pasemos a la funcion.
La funcion llamada incr sera para contar las veces que incrementamos a contador, observen como tomamos a contador pero de forma global, despues crearemos un objeto para almacenar cual es el thread actual llamado t, luego tenemos un bucle for donde contara gracias al rango que delimitamos con el valor pasado a la funcion, dentro de este creamos a actual y le asignamos el valor de contador, generamos una pausa, actualizamos a contador con el valor de actual mas uno, volvemos a hacer una pausa, para despues mostrar cual es el nombre de nuestro actual thread y el valor que tiene contador para despues repetir el ciclo hasta el final, con esto terminamos la funcion.
Lo siguiente sera generar el codigo que utilizara la funcion, para ello primero definimos una variable como n y el valor de 5, despues creamos dos threads donde le pasaremos la funcion a target, le pasamos un nombre de identificacion y los argumentos de la funcion, en este caso n, despues de generados los dos threads, los iniciamos con start y los detenemos con join, luego por ultimo mostramos el valor final de contador, si analizamos el codigo pasamos el valor de 5 para los dos threads, es decir que deberia hacer dos ciclos de 5 y devolvernos el valor de 10 pero veamos que sucede
En el video podemos observar como nos devuelve cual es el thread que esta trabajando en el momento y cual es el valor de contador asignado, pero como podemos observar en ningun momento nos devuelve el valor de 10, va a oscilar entre 5 y 7 pero nunca sera 10, esto es debido a lo que comentamos cuando hablamos de la race condition, que al momento de trabajar con dos threads el planificador sera el encargado de decidir a quien darle prioridad y esto puede generar una pausa y recuperacion innecesaria que afecta a nuestro resultado, pero como podemos solucionar al igual que hablamos en ese post y para ello del codigo anterior haremos un par de modificaciones:
race_condition.py
import threading
from time import sleep
from random import random
contador = 0
randsleep = lambda: sleep(0.1 * random())
lock_incr = threading.Lock()
def incr(n):
global contador
t = threading.current_thread()
for contar in range(n):
with lock_incr:
actual = contador
randsleep()
contador = actual + 1
randsleep()
print(f'{t.name} => {contador}')
n = 5
t1 = threading.Thread(target=incr,name='t1', args=(n, ))
t2 = threading.Thread(target=incr,name='t2', args=(n, ))
t1.start()
t2.start()
t1.join()
t2.join()
print(f'Contador: {contador}')
El codigo es muy similar pero agregamos la siguiente linea junto con el objeto randsleep:
lock_incr = threading.Lock()
Este objeto contendra un lock para nuestra funcion, sin mas parametros ni nada que se le asemeje, veamos la siguiente modificacion:
def incr(n):
global contador
t = threading.current_thread()
for contar in range(n):
with lock_incr:
actual = contador
randsleep()
contador = actual + 1
randsleep()
print(f'{t.name} => {contador}')
En este caso agregamos dentro del bucle for un with para el objeto lock_incr, es decir que ese lock se ejecutara hasta que termine, dentro del mismo tenemos las mismas instrucciones que hubo en el bucle for, lo bueno de trabajar de esta forma es que nos garantizamos que el lock exista mientras el thread trabaja y se cierra cuando el thread termino, si lo probamos sucedera lo siguiente:
tinchicus@dbn001vrt:~/lenguajes/python$ python3 race_condition.py
t1 => 1
t1 => 2
t1 => 3
t1 => 4
t1 => 5
t2 => 6
t2 => 7
t2 => 8
t2 => 9
t2 => 10
Contador: 10
tinchicus@dbn001vrt:~/lenguajes/python$
Como podemos ver ahora si lo que nosotros necesitamos, esto es gracias al lock dado que ahora el planificador por mas que intente cambiar el thread el otro debera esperar hasta que el lock sea liberado, como dijimos los problemas de race condition pueden solucionarse con lock pero no se olviden que el deadlock siempre estara acechando.
En resumen, hoy hemos visto como es la race condition en la vida real, hemos visto como se puede crear en un codigo aparentemente bien diseñado, tambien hemos visto como solucionarlo por medio de lock, espero les haya sido util sigueme en tumblr, Twitter o Facebook para recibir una notificacion cada vez que subo un nuevo post en este blog, nos vemos en el proximo post.


Donación
Es para mantenimento del sitio, gracias!
$1.50
