Iniciación a la programación concurrente
Noción de rendimiento
Antes de hablar de rendimiento, hay que saber analizar cuáles son los paradigmas utilizados, cuál es la complejidad del programa y, para terminar, cuál es la arquitectura de hardware necesaria para que el programa funcione.
En efecto, hay muchos detalles sobre los que el programa no tiene ningún control. Se puede citar, por ejemplo, la naturaleza del sistema operativo sobre el que funciona, la cantidad y el consumo de otros programas que funcionan al mismo tiempo, la cantidad de memoria, el procesador o incluso el ancho de banda de la red.
Hay que sabe que en esta parte no hablamos del rendimiento en el sentido de optimización a nivel de la algoritmia del código, sino de soluciones a nivel de la arquitectura de la aplicación en sí misma.
1. Programación bloqueante
Todo programa pasa por fases en las que está inactivo. Esto se puede deber al hecho de que espera alguna cosa del usuario:
data = read("Indique un dato")
O porque esperamos un recurso cualquiera: I/O, disco duro, periférico o incluso de la red:
import requests
response = requests.get('http://inspyration.org')
En este ejemplo, el programa espera la resolución de nombres (DNS), después de la respuesta del servidor que potencialmente va a poner la consulta en la cola de espera, y después trabajar por su parte durante un pequeño instante, antes de empezar a enviar su respuesta.
Tanto en un caso como en el otro, mientras que un programa espere el recurso, está bloqueado: no hará nada más. Según el contexto, esto puede ser aceptable, porque se puede permitir perder hasta 300, incluso 500 milisegundos, sin que el usuario se vea afectado.
Sin embargo, en otros contextos, esto es peor:
from bs4 import BeautifulSoup
soup = BeautifulSoup(response.content, "html.parser")
image_urls = [img.get("src") for img in soup.find_all("img")]
image_contents...
Terminología
En esta sección se presentarán algunas nociones técnicas que es necesario entender, para poder tomar decisiones acertadas.
1. Proceso
Un proceso es la ejecución de un conjunto de instrucciones a través del uso de recursos físicos, siendo los dos principales la memoria RAM, en la que se almacena el entorno de ejecución y el procesador asignado.
Las operaciones de creación, gestión y destrucción de un proceso son gestionadas por el sistema operativo, no directamente por Python. Python ofrece herramientas de alto nivel para crear, gestionar y destruir estos procesos que siguen siendo relativamente sencillos pero que usan en realidad el sistema operativo que hace la parte principal del trabajo.
Y afortunadamente, porque este último es extremamente complejo e implica nociones de programación de sistemas muy específicos. El proceso se gestiona por un planificador que depende del sistema operativo. Este último se encarga de poner a disposición los recursos (memoria, tiempo de procesador, etc.) y vigilar que cada proceso acceda equitativamente a ellos. Si hay varios procesadores, los procesos también se distribuyen entre ellos de manera uniforme.
Un programa ejecutado por un usuario, puede corresponder a un único proceso o a varios. Por ejemplo, durante el lanzamiento, Apache crea seis procesos:
$ ps ax | grep apache
1569 ? Ss 0:00 /usr/sbin/apache2 -k start
1584 ? S 0:00 /usr/sbin/apache2 -k start
1585 ? S 0:00 /usr/sbin/apache2 -k start
1586 ? S 0:00 /usr/sbin/apache2 -k start
1587 ? S 0:00...
Presentación de los paradigmas
1. Programación asíncrona
Cuando la problemática principal es el tiempo I/O, la solución ideal es programar de modo asíncrono. La idea es que el programa continúe funcionando con un proceso que solo tenga un hilo de ejecución, de forma que no haya que molestarse en gestionar tareas o procesos, que son herramientas relativamente engorrosas de implementar.
La programación asíncrona consiste en declarar que una de las tareas que el programa debe ejecutar sea potencialmente bloqueante (esperar una I/O y no tener nada que hacer mientras tanto) y que el programa pueda entonces seguir ejecutando el resto del código mientras espera.
Por ejemplo, este es uno de los puntos fuertes de JavaScript, un lenguaje que está diseñado con objetivos particulares: es ejecutado por un navegador y es el navegador el que gestiona los hilos y procesos de ejecución. Por lo tanto, JavaScript siempre tendrá, en ese contexto, un único hilo de ejecución y un solo proceso; dando por hecho que ejecuta instrucciones en un contexto de red, siempre hay mucha espera I/O.
La mejor opción para mejorar el rendimiento para este lenguaje es aprovechar la asincronía, aspecto en el que destaca, ya que todos los esfuerzos se concentran en este objetivo.
Así pues, cuando alabamos los méritos de Node.js, no se trata más de alardear del buen uso de las capacidades del proprio lenguaje. Node.js simplemente hace un buen uso de la asincronía y sabe cómo aprovecharla. Esto es también lo que hacen otros servidores Python muy antiguos y conocidos perfectamente desde hace años, tales como Tornado o Twisted, pero cada uno a su manera.
Uno de los ejes de mejora de la rama 3.x de Python es justamente el desarrollo de estas capacidades de gestión asíncrona. El capítulo Programación asíncrona: iniciación presenta estas funcionalidades y mustra algunos ejemplos concretos.
2. Programación en paralelo
La programación en paralelo es el acto de añadir la potencia a la CPU de la misma máquina, creando varios hilos de ejecución o varios procesos para repartir la carga de trabajo.
Utilizar la programación en paralelo hace necesario un poco de trabajo, pero en Python esto no es una tarea insuperable. Sin embargo, esto no aportará...