Home
Implementando hilos en Python

Implementando hilos en Python

Python y los hilos son una combinación poderosa a la hora de implemetar tus aplicaciones.

Cuando leas el artículo completo y hagas tu primera app con hilos en Python, te sentirás como Neo al ver el código de The Matrix. Si no has visto la película (shame on you), no lo digas a nadie y mírala cuando puedas 🙂

Para comenzar, vamos a aclarar alguna terminología:

  • Concurrencia es cuando dos o mas tareas pueden iniciar, correr, y completarse en periodos de tiempo superpuestos. Esto no necesariamente significa que ellos estarán corriendo al mismo instante. Eje. multitarea en una maquina de un solo núcleo.
  • Paralelismo es cuando dos o mas tareas son ejecutadas simultáneamente.
  • Un hilo es una secuencia de instrucciones dentro de un Este puede ser tomado como un proceso ligero. Los hilos comparten el mismo espacio de memoria.
  • Un proceso es una instancia de un programa corriendo en un computador el cual puede contener uno o mas hilos. Un proceso su espacio de memoria independiente.

El modulo threading es usado para trabajar con hilos en Python.

La implementación CPython tiene un  Interprete de Bloqueo Global (GIL) el cual permite que solo un hilo este activo en el interprete a la vez. Esto significa que los hilos no pueden ser usados en ejecución paralela del código de  Python. Mientras que el calculo de CPU en paralelo no es posible,  las operaciones en paralelo de E/S son posibles usando hilos. Esto es porque  las operaciones E/S liberan el GIL.

En que son usados los hilos en Python?

  • En aplicaciones GUI para mantener la respuesta en la interfaz de usuario.
  • Tareas E/S (redes E/S o sistema de archivos E/S)

Los hilos no pueden ser usados por las tareas vinculadas al  CPU. Usar hilos para las tareas vinculadas al CPU  resultaran en un peor desempeño comparado al uso de un solo hilo.

El siguiente ejemplo demuestra el uso de hilos para  sistema de archivos de E/S.

Una cola es usada para almacenar los archivos que necesitan ser procesados. Un diccionario es usado para almacenar los nombres de los archivos de entrada y salida. La función  process_queue() es usada para recuperar elementos de la cola y las operaciones de copia. La operación de copia se realiza en la función  copy_op función usada por el modulo shutil.

import threading
from queue import Queue
import time
import shutil

print_lock = threading.Lock()

def copy_op(file_data):
    with print_lock:
        print("Starting thread : {}".format(threading.current_thread().name))

    mydata = threading.local()
    mydata.ip, mydata.op = next(iter(file_data.items()))

    shutil.copy(mydata.ip, mydata.op)

    with print_lock:
        print("Finished thread : {}".format(threading.current_thread().name))

def process_queue():
    while True:
        file_data = compress_queue.get()
        copy_op(file_data)
        compress_queue.task_done()

compress_queue = Queue()

output_names = [{'v1.mp4' : 'v11.mp4'},{'v2.mp4' : 'v22.mp4'}]

for i in range(2):
    t = threading.Thread(target=process_queue)
    t.daemon = True
    t.start()

start = time.time()

for file_data in output_names:
    compress_queue.put(file_data)

compress_queue.join()

print("Execution time = {0:.5f}".format(time.time() - start))

Nota : El v1.mp4 y v2.mp4 son de 250MB cada uno.

Resultados

  • 7 a 10 segundos fue el tiempo tomado cuando se uso un hilo.
  • 4.5 a 5.5 segundos fue el tiempo tomado  cuando se uso dos hilos.

Entonces esta claro que los hilos pueden ser usados en paralelo para el sistema de archivos de E/S.

El siguiente ejemplo evidencia  el uso de hilos para redes E/S  usando la librería  requests . Este es un ejemplo de como jugar con el uso de hilos para redes E/S.

import threading
from queue import Queue
import requests
import bs4
import time

print_lock = threading.Lock()
def get_url(current_url):

    with print_lock:
        print("\nStarting thread {}".format(threading.current_thread().name))
    res = requests.get(current_url)
    res.raise_for_status()

    current_page = bs4.BeautifulSoup(res.text,"html.parser")
    current_title = current_page.select('title')[0].getText()

    with print_lock:
        print("{}\n".format(threading.current_thread().name))
        print("{}\n".format(current_url))
        print("{}\n".format(current_title))
        print("\nFinished fetching : {}".format(current_url))

def process_queue():
    while True:
        current_url = url_queue.get()
        get_url(current_url)
        url_queue.task_done()
url_queue = Queue()

url_list = ["https://www.google.com"]*5

for i in range(5):
    t = threading.Thread(target=process_queue)
    t.daemon = True
    t.start()
start = time.time()

for current_url in url_list:
url_queue.put(current_url)
url_queue.join()
print(threading.enumerate())
print("Execution time = {0:.5f}".format(time.time() - start))

Resultados

  • Un solo hilo: 4 segundos
  • Dos hilos: 3 segundos
  • Cinco hilos: 2 segundos.

En las  redes E/S, la mayoría de las veces  se gasta tiempo esperando por la respuesta de el URL, por lo tanto es otro caso donde el uso de hilos mejora el rendimiento.

Permíteme demostrar porque es una mala usar hilos para tareas vinculadas al CPU. En el siguiente programa una cola contiene números. La tarea es encontrar la suma de los números primos menores o iguales al numero dado. Esto es claramente una tarea vinculada al CPU

import threading
from queue import Queue
import time

list_lock = threading.Lock()

def find_rand(num):
    sum_of_primes = 0
    ix = 2
    while ix <= num:
        if is_prime(ix):
            sum_of_primes += ix
        ix += 1
   sum_primes_list.append(sum_of_primes)
   def is_prime(num):
    if num <= 1:
        return False
    elif num <= 3:
        return True
    elif num%2 == 0 or num%3 == 0:
        return False
    i = 5
    while i*i <= num:
        if num%i == 0 or num%(i+2) == 0:
            return False
        i += 6
    return True

def process_queue():
    while True:
        rand_num = min_nums.get()
        find_rand(rand_num)
        min_nums.task_done()

min_nums = Queue()

rand_list = [1000000, 2000000, 3000000]
sum_primes_list = list()

for i in range(2):
    t = threading.Thread(target=process_queue)
    t.daemon = True
    t.start()

start = time.time()

for rand_num in rand_list:
    min_nums.put(rand_num)

min_nums.join()

end_time = time.time()

sum_primes_list.sort()
print(sum_primes_list)

print("Execution time = {0:.5f}".format(end_time - start))

Resultados

  • Un solo hilo : 25.5 segundos
  • Dos hilos : 28 segundos

Los resultados son muy claros : no usar hilos mejora el rendimiento  de las tareas vinculadas al  CPU. Siempre terminaras con un peor rendimiento.

Para la ejecución en paralelo de tareas, el modulo  multiprocessing puede ser usado.

En el siguiente ejemplo nosotros tomamos la misma tarea  anterior y la entrada de procesos en paralelo usando el modulo multiprocessing.

from multiprocessing import Pool
import time

def sum_prime(num):
      sum_of_primes = 0

    ix = 2

    while ix <= num:
        if is_prime(ix):
            sum_of_primes += ix
        ix += 1

    return sum_of_primes

def is_prime(num):
    if num <= 1:
        return False
    elif num <= 3:
        return True
    elif num%2 == 0 or num%3 == 0:
        return False
    i = 5
    while i*i <= num:
        if num%i == 0 or num%(i+2) == 0:
            return False
        i += 6
    return True

if __name__ == '__main__':
    start = time.time()
    with Pool(1) as p:
        print(p.map(sum_prime, [1000000, 2000000, 3000000]))
    print("Time taken = {0:.5f}".format(time.time() - start))

Resultados

  • Usando un solo proceso : 27 segundos
  • Usando dos procesos : 19 segundos
  • Usando tres procesos : 18 segundos

Nosotros vemos una enorme mejora desde el uso de un solo proceso al uso de dos procesos. Pero el salto de dos procesos a tres procesos es mínimo. La razón detrás de esto es mi hardware. Yo tengo un CPU de núcleo doble (portátil) con  Hyperthreading (El sistema operativo detecta esto como cuatro CPU debido al Hyperthreading). Por otro lado, yo se que  Intel’s Hyperthreading no es un reemplazo para mas núcleos.  El ejemplo  suscribe una verificación de esto.

Por lo tanto el uso del modulo  multiprocessing da como resultado la completa utilización del CPU.

La comunicación interna de procesos puede ser lograda usando colas o pilas. La cola en el modulo multiprocessing trabaja de manera similar al modulo queue usado para demostrar como el modulo threading trabaja  por tanto no voy a cubrirlo.

Otro mecanismo útil de comunicación entre procesos es una tubería. Una tubería es un canal de comunicación (de dos direcciones).

Nota : Leer  o escribir en el mismo extremo de la tubería simultáneamente puede resultar en la corrupción de los datos.

El siguiente es un ejemplo básico:

import multiprocessing as mp
import os

def info(conn):
    conn.send("Hello from {}\nppid = {}\npid={}".format(mp.current_process().name, os.getppid(), os.getpid()))
    conn.close()

if __name__ == '__main__':

    parent_conn, child_conn = mp.Pipe()
    p = mp.Process(target=info, args=(child_conn,))
    p.daemon = True
    p.start()
    print(parent_conn.recv())

Salida:

Hello from Process-1
ppid = 18621
pid=18622

Como ven, usar hilos tiene un impacto tremendo en la ejecución de tareas en paralelo. Y en Python, basta com importar la librería adecuada para obtener la capacidad de trabajar con ellos.

Leave a Comment

*

*

A %d blogueros les gusta esto: