Programación en paralelo
Uso de un hilo de ejecución
1. Gestionar un hilo de ejecución
a. Presentación
Hace tiempo que Python propuso un módulo llamado thread, que ofrecía un API de bajo nivel y que permitía crear hilos de ejecución, según diferentes soluciones que podrían corresponder a las capacidades de Python en este dominio.
La implementación CPython, teniendo partes completas de su código incompatibles con una creación propia de hilos de ejecución, ha evolucionado y finalmente ha quedado obsoleto con la finalización de un API de alto nivel, threading, que no solo es más sencillo de utilizar y está más próximo a las necesidades, sino que también es más fiable.
En Python 3.x, el antiguo y obsoleto módulo de bajo nivel, se ha eliminado (vea la PEP (Python Enhancement Proposals) en la documentación de Python bajo el capítulo Obsolete en: https://www.python.org/dev/peps/pep-3108/), y el nuevo módulo se beneficia de todos los esfuerzos añadidos a la implementación de CPython, para permitir tener varios hilos de ejecución.
Como veremos, la problemática no es la creación y ejecución en paralelo de hilos de ejecución, sino más bien la gestión de los recursos que comparten y sus comunicaciones.
b. Crear
Para crear un hilo de ejecución, hay que utilizar el módulo de alto nivel:
>>> from threading import Thread
Para las necesidades de este ejemplo, también cargamos los siguientes módulos:
>>> from time import time, ctime, sleep
A continuación, se muestra la declaración de un hilo de ejecución, siguiendo su uso:
>>> class Worker(Thread):
... def __init__(self, name, delay):
... self.delay = delay
... Thread.__init__(self, name=name)
... def run(self):
... for i in range(5):
... print("%s: llamada %s, %s" % (self.getName(), i, ctime(time())))
... sleep(self.delay)
...
>>> try:
... t1 = Worker("T1", 2)
... t2 = Worker("T2"...
Uso de proceso
1. Gestionar un proceso
a. Presentación
En un determinado número de planes, visto desde la interfaz de alto nivel proporcionada por Python, la multitarea (o multi-hilo), se parece al multiproceso. Pero la semejanza termina en las listas de métodos propuestos por los objetos.
La problemática por resolver es más compleja. Su ventaja es permitir ejecutar dos acciones al mismo tiempo sobre dos procesadores diferentes y, así, reducir su tiempo de ejecución.
Un proceso es independiente, incorpora su propio entorno de ejecución. También está relacionado con el proceso que lo crea, que puede ser la consola Python, la máquina virtual Python u otro proceso Python.
b. Crear
Crear un proceso es relativamente sencillo. A continuación, se muestran los módulos necesarios:
>>> from multiprocessing import Process
>>> from time import sleep
Veamos ahora un trabajo que se va a adjuntar al proceso:
>>> def work(name):
... print('Inicio del trabajo: %s' % name)
... for j in range(10):
... for i in range(10):
... sleep(0.01)
... print('.', sep='', end='')
... print('.')
... print('Fin del trabajo: %s' % name)
...
Es suficiente con crear el proceso y arrancarlo:
>>> p = Process(target=work, args=('Test',))
>>> p.start()
>>> p.join()
Vemos aquí el resultado:
Inicio del trabajo: Prueba
...........
...........
...........
...........
...........
...........
...........
...........
...........
...........
Fin del trabajo: Test
Vamos a destacar las características del proceso:
>>> print ('Flujo principal: %s' % os.getpid())
Flujo principal: 8879
El PID (Process ID) padre del proceso es este número:
>>> def work(name):
... print('Inicio del trabajo: %s' % name)
... print('Pid: %s, padre: %s'...
Ejecutar de forma asíncrona
1. Introducción
Hasta ahora, se ha visto cómo crear hilos de ejecución y gestionarlos con herramientas de alto nivel, que permiten asignar el trabajo a cada hilo de ejecución, cómo utilizarlos lo mejor posible y gestionar su ejecución de manera adecuada.
También hemos visto cómo crear procesos y gestionarlos siempre con las herramientas de alto nivel.
Estas operaciones, respecto a lo que implican como técnica para los lenguajes de bajo nivel, son relativamente simples de utilizar y hemos visto diferentes casos de aplicación y distintas implementaciones posibles.
También es posible realizar operaciones de más bajo nivel todavía, como el uso del fork, que sigue estando disponible, incluso si este capítulo no insiste especialmente en esta funcionalidad, prefiriendo poner el foco en el alto nivel.
De esta manera, los que conocen el fork en C, serán capaces de efectuar la misma operación en Python, porque se trata de lo mismo. Además, las reglas de programación son comunes a todos los lenguajes.
Sin embargo, el uso del bajo nivel, incluso de los dos módulos de alto nivel que son threading y multiprocessing, añade riesgos y hace necesario cierto dominio, incluso si esto es mucho más sencillo que con los lenguajes de más bajo nivel, como el C.
El gran riesgo es crear un proceso o un hilo de ejecución sobre el que perdamos el control. Si sucede algo así, el hilo de ejecución o el proceso puede llegar a ocupar el 100 % del procesador y no devolver el control por un largo tiempo. Esto puede suponer un problema para el resto de los procesos que están funcionando en la máquina (más allá de los otros hilos de ejecución o procesos Python) y esto puede provocar el fallo del sistema operativo.
Afortunadamente, las herramientas de alto nivel que hemos vistos hasta ahora, utilizadas correctamente permiten evitar esto. Recurrir a sleep dentro de los hilos de ejecución y procesos, es costoso pero necesario para pasar el control a los otros si se necesita. Es mejor perder tiempo revisando el resto de proceso, quitar el control al que lo tiene para devolvérselo inmediatamente porque no se necesita, que perder el control de la máquina.
Este tipo de problemática, incluso simplificada, se debe dominar...