Multihilo en Python | Juego 2 (Sincronización)

Este artículo analiza el concepto de sincronización de subprocesos en caso de subprocesos múltiples en el lenguaje de programación Python.

Sincronización entre hilos

La sincronización de subprocesos se define como un mecanismo que asegura que dos o más subprocesos concurrentes no ejecuten simultáneamente algún segmento de programa en particular conocido como sección crítica .

La sección crítica se refiere a las partes del programa donde se accede al recurso compartido.

Por ejemplo, en el siguiente diagrama, 3 subprocesos intentan acceder al recurso compartido o a la sección crítica al mismo tiempo.

Los accesos simultáneos a recursos compartidos pueden conducir a una condición de carrera .

Una condición de carrera ocurre cuando dos o más subprocesos pueden acceder a datos compartidos e intentan cambiarlos al mismo tiempo. Como resultado, los valores de las variables pueden ser impredecibles y variar según los tiempos de los cambios de contexto de los procesos.

Considere el siguiente programa para comprender el concepto de condición de carrera:

import threading
  
# global variable x
x = 0
  
def increment():
    """
    function to increment global variable x
    """
    global x
    x += 1
  
def thread_task():
    """
    task for thread
    calls increment function 100000 times.
    """
    for _ in range(100000):
        increment()
  
def main_task():
    global x
    # setting global variable x as 0
    x = 0
  
    # creating threads
    t1 = threading.Thread(target=thread_task)
    t2 = threading.Thread(target=thread_task)
  
    # start threads
    t1.start()
    t2.start()
  
    # wait until threads finish their job
    t1.join()
    t2.join()
  
if __name__ == "__main__":
    for i in range(10):
        main_task()
        print("Iteration {0}: x = {1}".format(i,x))

Producción:

Iteration 0: x = 175005
Iteration 1: x = 200000
Iteration 2: x = 200000
Iteration 3: x = 169432
Iteration 4: x = 153316
Iteration 5: x = 200000
Iteration 6: x = 167322
Iteration 7: x = 200000
Iteration 8: x = 169917
Iteration 9: x = 153589

En el programa anterior:

  • Se crean dos subprocesos t1 y t2 en la función main_task y la variable global x se establece en 0.
  • Cada subproceso tiene una función objetivo thread_task en la que la función de incremento se llama 100000 veces.
  • La función de incremento incrementará la variable global x en 1 en cada llamada.

El valor final esperado de x es 200000 pero lo que obtenemos en 10 iteraciones de la función main_task son algunos valores diferentes.

Esto sucede debido al acceso simultáneo de subprocesos a la variable compartida x . Esta imprevisibilidad en el valor de x no es más que una condición de carrera .

A continuación se muestra un diagrama que muestra cómo puede ocurrir la condición de carrera en el programa anterior:

Tenga en cuenta que el valor esperado de x en el diagrama anterior es 12, pero debido a la condición de carrera, ¡resulta ser 11!

Por lo tanto, necesitamos una herramienta para la sincronización adecuada entre múltiples subprocesos.

Uso de candados

El módulo de subprocesos proporciona una clase de bloqueo para hacer frente a las condiciones de carrera. El bloqueo se implementa mediante un objeto Semaphore proporcionado por el sistema operativo.

Un semáforo es un objeto de sincronización que controla el acceso de múltiples procesos/subprocesos a un recurso común en un entorno de programación paralelo. Es simplemente un valor en un lugar designado en el almacenamiento del sistema operativo (o kernel) que cada proceso/subproceso puede verificar y luego cambiar. Dependiendo del valor que se encuentre, el proceso/subproceso puede usar el recurso o encontrará que ya está en uso y debe esperar un tiempo antes de volver a intentarlo. Los semáforos pueden ser binarios (0 o 1) o pueden tener valores adicionales. Por lo general, un proceso/subproceso que usa semáforos verifica el valor y luego, si usa el recurso, cambia el valor para reflejar esto, de modo que los usuarios de semáforos posteriores sepan que deben esperar.

La clase de bloqueo proporciona los siguientes métodos:

  • adquirir ([bloqueo]) : Para adquirir un bloqueo. Un bloqueo puede ser bloqueante o no bloqueante.
    • Cuando se invoca con el argumento de bloqueo establecido en True (el valor predeterminado), la ejecución del subproceso se bloquea hasta que se desbloquea el bloqueo, luego el bloqueo se establece en bloqueado y devuelve True .
    • Cuando se invoca con el argumento de bloqueo establecido en False , la ejecución del subproceso no se bloquea. Si el bloqueo está desbloqueado, configúrelo como bloqueado y devuelva True ; de ​​lo contrario, devuelva False inmediatamente.
  • release() : Para liberar un bloqueo.
    • Cuando la cerradura esté bloqueada, reiníciela a desbloqueada y regrese. Si otros subprocesos están bloqueados esperando que el bloqueo se desbloquee, permita que exactamente uno de ellos continúe.
    • Si el bloqueo ya está desbloqueado, se genera un ThreadError .

Considere el ejemplo dado a continuación:

import threading
  
# global variable x
x = 0
  
def increment():
    """
    function to increment global variable x
    """
    global x
    x += 1
  
def thread_task(lock):
    """
    task for thread
    calls increment function 100000 times.
    """
    for _ in range(100000):
        lock.acquire()
        increment()
        lock.release()
  
def main_task():
    global x
    # setting global variable x as 0
    x = 0
  
    # creating a lock
    lock = threading.Lock()
  
    # creating threads
    t1 = threading.Thread(target=thread_task, args=(lock,))
    t2 = threading.Thread(target=thread_task, args=(lock,))
  
    # start threads
    t1.start()
    t2.start()
  
    # wait until threads finish their job
    t1.join()
    t2.join()
  
if __name__ == "__main__":
    for i in range(10):
        main_task()
        print("Iteration {0}: x = {1}".format(i,x))

Producción:

Iteration 0: x = 200000
Iteration 1: x = 200000
Iteration 2: x = 200000
Iteration 3: x = 200000
Iteration 4: x = 200000
Iteration 5: x = 200000
Iteration 6: x = 200000
Iteration 7: x = 200000
Iteration 8: x = 200000
Iteration 9: x = 200000

Tratemos de entender el código anterior paso a paso:

  • En primer lugar, se crea un objeto Lock usando:
      lock = threading.Lock()
    
  • Luego, el bloqueo se pasa como argumento de la función de destino:
      t1 = threading.Thread(target=thread_task, args=(lock,))
      t2 = threading.Thread(target=thread_task, args=(lock,))
    
  • En la sección crítica de la función de destino, aplicamos el bloqueo mediante el método lock.acquire() . Tan pronto como se adquiere un bloqueo, ningún otro subproceso puede acceder a la sección crítica (aquí, la función de incremento ) hasta que se libera el bloqueo mediante el método lock.release() .
      lock.acquire()
      increment()
      lock.release()
    

    Como puede ver en los resultados, el valor final de x siempre es 200000 (que es el resultado final esperado).

Aquí hay un diagrama a continuación que muestra la implementación de bloqueos en el programa anterior:

Esto nos lleva al final de esta serie de tutoriales sobre subprocesos múltiples en Python .
Finalmente, aquí hay algunas ventajas y desventajas de los subprocesos múltiples:

ventajas:

  • No bloquea al usuario. Esto se debe a que los hilos son independientes entre sí.
  • Es posible un mejor uso de los recursos del sistema ya que los subprocesos ejecutan tareas en paralelo.
  • Rendimiento mejorado en máquinas multiprocesador.
  • Los servidores de subprocesos múltiples y las GUI interactivas utilizan subprocesos múltiples exclusivamente.

Desventajas:

  • A medida que aumenta el número de subprocesos, aumenta la complejidad.
  • Es necesaria la sincronización de los recursos compartidos (objetos, datos).
  • Es difícil de depurar, el resultado a veces es impredecible.
  • Puntos muertos potenciales que conducen a la inanición, es decir, es posible que algunos subprocesos no se sirvan con un mal diseño
  • La construcción y sincronización de subprocesos consume mucha CPU/memoria.

Este artículo es una contribución de Nikhil Kumar . Si le gusta GeeksforGeeks y le gustaría contribuir, también puede escribir un artículo usando contribuya.geeksforgeeks.org o envíe su artículo por correo a contribuya@geeksforgeeks.org. Vea su artículo que aparece en la página principal de GeeksforGeeks y ayude a otros Geeks.

Escriba comentarios si encuentra algo incorrecto o si desea compartir más información sobre el tema tratado anteriormente.

Publicación traducida automáticamente

Artículo escrito por GeeksforGeeks-1 y traducido por Barcelona Geeks. The original can be accessed here. Licence: CCBY-SA

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *