Intercomunicación de hilos

En la vida real, si un equipo de personas está trabajando en una tarea común, debería haber comunicación entre ellos para terminar la tarea correctamente. La misma analogía se aplica también a los hilos. En programación, para reducir el tiempo ideal del procesador, creamos múltiples subprocesos y asignamos diferentes subtareas a cada subproceso. Por lo tanto, debe haber una facilidad de comunicación y deben interactuar entre sí para terminar el trabajo de manera sincronizada.

Considere los siguientes puntos importantes relacionados con la intercomunicación de subprocesos:

  • No performance gain - Si no podemos lograr una comunicación adecuada entre subprocesos y procesos, las ganancias de rendimiento de la concurrencia y el paralelismo no sirven de nada.

  • Accomplish task properly - Sin un mecanismo de intercomunicación adecuado entre subprocesos, la tarea asignada no se puede completar correctamente.

  • More efficient than inter-process communication - La comunicación entre subprocesos es más eficiente y fácil de usar que la comunicación entre procesos porque todos los subprocesos dentro de un proceso comparten el mismo espacio de direcciones y no necesitan usar memoria compartida.

Estructuras de datos de Python para una comunicación segura para subprocesos

El código multiproceso presenta el problema de pasar información de un hilo a otro hilo. Las primitivas de comunicación estándar no resuelven este problema. Por lo tanto, necesitamos implementar nuestro propio objeto compuesto para compartir objetos entre subprocesos para que la comunicación sea segura para subprocesos. A continuación se muestran algunas estructuras de datos, que proporcionan una comunicación segura para subprocesos después de realizar algunos cambios en ellas:

Conjuntos

Para usar la estructura de datos de conjunto de una manera segura para subprocesos, necesitamos extender la clase de conjunto para implementar nuestro propio mecanismo de bloqueo.

Ejemplo

Aquí hay un ejemplo de Python para extender la clase:

class extend_class(set):
   def __init__(self, *args, **kwargs):
      self._lock = Lock()
      super(extend_class, self).__init__(*args, **kwargs)

   def add(self, elem):
      self._lock.acquire()
	  try:
      super(extend_class, self).add(elem)
      finally:
      self._lock.release()
  
   def delete(self, elem):
      self._lock.acquire()
      try:
      super(extend_class, self).delete(elem)
      finally:
      self._lock.release()

En el ejemplo anterior, un objeto de clase llamado extend_class se ha definido que se hereda aún más de Python set class. Se crea un objeto de bloqueo dentro del constructor de esta clase. Ahora, hay dos funciones:add() y delete(). Estas funciones están definidas y son seguras para subprocesos. Ambos confían en elsuper funcionalidad de clase con una excepción clave.

Decorador

Este es otro método clave para la comunicación segura con subprocesos es el uso de decoradores.

Ejemplo

Considere un ejemplo de Python que muestra cómo usar decoradores & mminus;

def lock_decorator(method):

   def new_deco_method(self, *args, **kwargs):
      with self._lock:
         return method(self, *args, **kwargs)
return new_deco_method

class Decorator_class(set):
   def __init__(self, *args, **kwargs):
      self._lock = Lock()
      super(Decorator_class, self).__init__(*args, **kwargs)

   @lock_decorator
   def add(self, *args, **kwargs):
      return super(Decorator_class, self).add(elem)
   @lock_decorator
   def delete(self, *args, **kwargs):
      return super(Decorator_class, self).delete(elem)

En el ejemplo anterior, se ha definido un método decorador llamado lock_decorator que se hereda de la clase de método Python. Luego, se crea un objeto de bloqueo dentro del constructor de esta clase. Ahora, hay dos funciones: agregar () y eliminar (). Estas funciones están definidas y son seguras para subprocesos. Ambos se basan en una funcionalidad de clase superior con una excepción clave.

Liza

La estructura de datos de la lista es segura para subprocesos, estructura rápida y fácil para el almacenamiento temporal en memoria. En Cpython, el GIL protege contra el acceso concurrente a ellos. Como llegamos a saber que las listas son seguras para subprocesos, pero ¿qué pasa con los datos que se encuentran en ellas? En realidad, los datos de la lista no están protegidos. Por ejemplo,L.append(x)no es garantía de devolver el resultado esperado si otro hilo intenta hacer lo mismo. Esto se debe a que, aunqueappend() es una operación atómica y segura para subprocesos, pero el otro subproceso está tratando de modificar los datos de la lista de manera concurrente, por lo que podemos ver los efectos secundarios de las condiciones de carrera en la salida.

Para resolver este tipo de problema y modificar los datos de forma segura, debemos implementar un mecanismo de bloqueo adecuado, que además garantice que varios subprocesos no puedan encontrarse potencialmente en condiciones de carrera. Para implementar el mecanismo de bloqueo adecuado, podemos extender la clase como hicimos en los ejemplos anteriores.

Algunas otras operaciones atómicas en listas son las siguientes:

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()

Aquí -

  • L, L1, L2 son listas
  • D, D1, D2 son dictados
  • x, y son objetos
  • yo, j son ints

Colas

Si los datos de la lista no están protegidos, es posible que tengamos que enfrentar las consecuencias. Es posible que obtengamos o eliminemos datos incorrectos de las condiciones de la carrera. Por eso se recomienda utilizar la estructura de datos de cola. Un ejemplo real de cola puede ser una carretera de un solo carril, donde el vehículo entra primero y sale primero. Se pueden ver más ejemplos del mundo real de las colas en las taquillas y las paradas de autobús.

Las colas son, por defecto, una estructura de datos segura para subprocesos y no debemos preocuparnos por implementar un mecanismo de bloqueo complejo. Python nos proporciona la módulo para utilizar diferentes tipos de colas en nuestra aplicación.

Tipos de colas

En esta sección, ganaremos sobre los diferentes tipos de colas. Python proporciona tres opciones de colas para usar desde el<queue> módulo -

  • Colas normales (FIFO, primero en entrar, primero en salir)
  • LIFO, último en entrar, primero en salir
  • Priority

Aprenderemos sobre las diferentes colas en las secciones siguientes.

Colas normales (FIFO, primero en entrar, primero en salir)

Son las implementaciones de cola más utilizadas que ofrece Python. En este mecanismo de cola, quienquiera que llegue primero, recibirá el servicio primero. FIFO también se denomina colas normales. Las colas FIFO se pueden representar de la siguiente manera:

Implementación de Python de la cola FIFO

En Python, la cola FIFO se puede implementar con un solo hilo o con varios hilos.

Cola FIFO con un solo hilo

Para implementar la cola FIFO con un solo hilo, el Queueclass implementará un contenedor básico primero en entrar, primero en salir. Los elementos se agregarán a un "final" de la secuencia usandoput(), y se quita del otro extremo usando get().

Ejemplo

A continuación se muestra un programa de Python para la implementación de la cola FIFO con un solo hilo:

import queue

q = queue.Queue()

for i in range(8):
   q.put("item-" + str(i))

while not q.empty():
   print (q.get(), end = " ")

Salida

item-0 item-1 item-2 item-3 item-4 item-5 item-6 item-7

El resultado muestra que el programa anterior usa un solo hilo para ilustrar que los elementos se eliminan de la cola en el mismo orden en que se insertan.

Cola FIFO con varios subprocesos

Para implementar FIFO con múltiples subprocesos, necesitamos definir la función myqueue (), que se extiende desde el módulo de cola. El funcionamiento de los métodos get () y put () es el mismo que se discutió anteriormente al implementar la cola FIFO con un solo hilo. Luego, para que sea multiproceso, necesitamos declarar e instanciar los subprocesos. Estos subprocesos consumirán la cola en forma FIFO.

Ejemplo

A continuación se muestra un programa de Python para la implementación de la cola FIFO con múltiples subprocesos

import threading
import queue
import random
import time
def myqueue(queue):
   while not queue.empty():
   item = queue.get()
   if item is None:
   break
   print("{} removed {} from the queue".format(threading.current_thread(), item))
   queue.task_done()
   time.sleep(2)
q = queue.Queue()
for i in range(5):
   q.put(i)
threads = []
for i in range(4):
   thread = threading.Thread(target=myqueue, args=(q,))
   thread.start()
   threads.append(thread)
for thread in threads:
   thread.join()

Salida

<Thread(Thread-3654, started 5044)> removed 0 from the queue
<Thread(Thread-3655, started 3144)> removed 1 from the queue
<Thread(Thread-3656, started 6996)> removed 2 from the queue
<Thread(Thread-3657, started 2672)> removed 3 from the queue
<Thread(Thread-3654, started 5044)> removed 4 from the queue

LIFO, último en cola, primero en salir

Esta cola utiliza una analogía totalmente opuesta a las colas FIFO (primero en entrar, primero en salir). En este mecanismo de cola, el que llegue en último lugar obtendrá el servicio primero. Esto es similar a implementar la estructura de datos de la pila. Las colas LIFO resultan útiles al implementar la búsqueda en profundidad como algoritmos de inteligencia artificial.

Implementación de Python de la cola LIFO

En Python, la cola LIFO se puede implementar con un solo hilo o con varios hilos.

Cola LIFO con un solo hilo

Para implementar la cola LIFO con un solo hilo, el Queue La clase implementará un contenedor básico de último en entrar, primero en salir utilizando la estructura Queue.LifoQueue. Ahora, al llamarput(), los elementos se agregan en la cabeza del contenedor y se quitan de la cabeza también al usar get().

Ejemplo

A continuación se muestra un programa de Python para la implementación de la cola LIFO con un solo hilo:

import queue

q = queue.LifoQueue()

for i in range(8):
   q.put("item-" + str(i))

while not q.empty():
   print (q.get(), end=" ")
Output:
item-7 item-6 item-5 item-4 item-3 item-2 item-1 item-0

El resultado muestra que el programa anterior usa un solo hilo para ilustrar que los elementos se eliminan de la cola en el orden opuesto en el que se insertan.

Cola LIFO con varios subprocesos

La implementación es similar a la que hemos hecho con la implementación de colas FIFO con múltiples subprocesos. La única diferencia es que necesitamos usar elQueue clase que implementará un contenedor básico de último en entrar, primero en salir usando la estructura Queue.LifoQueue.

Ejemplo

A continuación se muestra un programa de Python para la implementación de la cola LIFO con múltiples subprocesos:

import threading
import queue
import random
import time
def myqueue(queue):
   while not queue.empty():
      item = queue.get()
      if item is None:
      break
	  print("{} removed {} from the queue".format(threading.current_thread(), item))
      queue.task_done()
      time.sleep(2)
q = queue.LifoQueue()
for i in range(5):
   q.put(i)
threads = []
for i in range(4):
   thread = threading.Thread(target=myqueue, args=(q,))
   thread.start()
   threads.append(thread)
for thread in threads:
   thread.join()

Salida

<Thread(Thread-3882, started 4928)> removed 4 from the queue
<Thread(Thread-3883, started 4364)> removed 3 from the queue
<Thread(Thread-3884, started 6908)> removed 2 from the queue
<Thread(Thread-3885, started 3584)> removed 1 from the queue
<Thread(Thread-3882, started 4928)> removed 0 from the queue

Cola de prioridad

En las colas FIFO y LIFO, el orden de los elementos está relacionado con el orden de inserción. Sin embargo, hay muchos casos en los que la prioridad es más importante que el orden de inserción. Consideremos un ejemplo del mundo real. Suponga que la seguridad en el aeropuerto está revisando a personas de diferentes categorías. Las personas del VVIP, el personal de la aerolínea, el oficial de aduanas, las categorías pueden ser verificadas con prioridad en lugar de ser verificadas sobre la base de la llegada, como ocurre con los plebeyos.

Otro aspecto importante que debe tenerse en cuenta para la cola de prioridad es cómo desarrollar un programador de tareas. Un diseño común es atender la mayor parte de las tareas de los agentes con prioridad en la cola. Esta estructura de datos se puede utilizar para recoger los elementos de la cola según su valor de prioridad.

Implementación de Python de Priority Queue

En Python, la cola de prioridad se puede implementar con un solo subproceso, así como con varios subprocesos.

Cola de prioridad con un solo hilo

Para implementar la cola de prioridad con un solo hilo, el Queue la clase implementará una tarea en el contenedor de prioridad usando la estructura Queue.PriorityQueue. Ahora, al llamarput(), los elementos se agregan con un valor donde el valor más bajo tendrá la prioridad más alta y, por lo tanto, se recuperará primero usando get().

Ejemplo

Considere el siguiente programa de Python para la implementación de la cola de prioridad con un solo hilo:

import queue as Q
p_queue = Q.PriorityQueue()

p_queue.put((2, 'Urgent'))
p_queue.put((1, 'Most Urgent'))
p_queue.put((10, 'Nothing important'))
prio_queue.put((5, 'Important'))

while not p_queue.empty():
   item = p_queue.get()
   print('%s - %s' % item)

Salida

1 – Most Urgent
2 - Urgent
5 - Important
10 – Nothing important

En el resultado anterior, podemos ver que la cola ha almacenado los elementos en función de la prioridad; menos valor tiene alta prioridad.

Cola de prioridad con subprocesos múltiples

La implementación es similar a la implementación de las colas FIFO y LIFO con múltiples subprocesos. La única diferencia es que necesitamos usar elQueue clase para inicializar la prioridad usando la estructura Queue.PriorityQueue. Otra diferencia es la forma en que se generaría la cola. En el ejemplo que se muestra a continuación, se generará con dos conjuntos de datos idénticos.

Ejemplo

El siguiente programa de Python ayuda en la implementación de la cola de prioridad con múltiples subprocesos:

import threading
import queue
import random
import time
def myqueue(queue):
   while not queue.empty():
      item = queue.get()
      if item is None:
      break
      print("{} removed {} from the queue".format(threading.current_thread(), item))
      queue.task_done()
      time.sleep(1)
q = queue.PriorityQueue()
for i in range(5):
   q.put(i,1)

for i in range(5):
   q.put(i,1)

threads = []
for i in range(2):
   thread = threading.Thread(target=myqueue, args=(q,))
   thread.start()
   threads.append(thread)
for thread in threads:
   thread.join()

Salida

<Thread(Thread-4939, started 2420)> removed 0 from the queue
<Thread(Thread-4940, started 3284)> removed 0 from the queue
<Thread(Thread-4939, started 2420)> removed 1 from the queue
<Thread(Thread-4940, started 3284)> removed 1 from the queue
<Thread(Thread-4939, started 2420)> removed 2 from the queue
<Thread(Thread-4940, started 3284)> removed 2 from the queue
<Thread(Thread-4939, started 2420)> removed 3 from the queue
<Thread(Thread-4940, started 3284)> removed 3 from the queue
<Thread(Thread-4939, started 2420)> removed 4 from the queue
<Thread(Thread-4940, started 3284)> removed 4 from the queue