Un poco de publi: curso presencial de Python en Madrid, con Securízame

Gracias a Securízame, y junto a cuatro compañeros, vamos a impartir un curso completo de Python. Empieza por la introducción al lenguaje, sigue con una parte específica para pentesters y otra para sysadmins, y termina con la parte más avanzada. Esa última es la que doy yo.

El índice completo está en la web de Securízame:

https://www.securizame.com/nuevos-cursos-online-y-presenciales-en-securizame-python-para-sysadmins-y-pentesters/

Si te quieres meter en Python, no te vendría mal alguien que te eche una mano y te guíe mientras aprendes, ¿verdad?

Échale un ojo al índice. Cuando lo veas no tendré que decirte nada más. Te convences solo.

Curso de Python avanzado

Como decía, a mí me toca la parte avanzada. ¿En qué consiste? Se centra en cuatro puntos:

  • Estructuración y gestión eficiente de proyectos.
  • Mejora e incremento del rendimiento.
  • Concurrencia y distribución de carga.
  • Despliegue de proyectos y uso de Docker con Python.

El índice completo y ampliado está aquí:

https://cursos.securizame.com/python-avanzado/

Para muestra un botón

Para abrir boca, te cuento un adelanto mínimo de lo que veremos en el curso.

Python y su inexistente paralelismo

Supongo que sabes lo que son los hilos. Es un concepto que existe en casi cualquier lenguaje de programación, y Python, cómo no, también nos permite usar hilos. ¿O no?

Cómo que no, si Python tiene la librería threading que sirve para crear hilos.

La librería existe, sí. Pero es una de las grandes mentirijillas de Python.

Por qué Python nos miente, ¿qué le he hecho yo?

Un poco de culturilla.

Cuando Guido van Rossum, el creador de Python, diseñó el lenguaje, tenía una cosa muy clara: tenía que ser simple. No solo el lenguaje, también el intérprete.

Con esa idea en la cabeza, creó el intérprete sin soporte para hilos. Con los años, los programadores reclamaban cada vez más ese soporte.

Pero meter hilos suponía complicar el intérprete. Y mucho. ¿Por qué? Porque el paralelismo trae, entre otros muchos problemas que hay que resolver:

  • Sincronización.
  • Condiciones de carrera.
  • Interbloqueos.
  • Fugas de memoria.

Python y los falsos hilos

Pese a todo, los programadores seguían pidiendo hilos. Lo solucionaron así:

  • Crearon el GIL (Global Interpreter Lock).
  • Crearon una librería que da al programador una API para crear y gestionar hilos.

Vale, ¿y esto qué es?

El GIL

Ya lo hemos dicho: meter hilos de verdad supone mucho esfuerzo y complica el intérprete. Lo que hace el GIL es simular la ejecución de hilos. Es decir:

  • Python crea un proceso y un hilo por defecto. Cualquier programa que hagamos se ejecuta en ese contexto.
  • Si creamos más hilos, Python y el GIL van alternando la ejecución de cada hilo en el procesador, de manera que solo se ejecuta un hilo a la vez. Conseguimos concurrencia, pero no paralelismo, que es lo que nos dice la intuición.

Vaya castaña, ¿verdad? Cuando te enteras de esto la primera vez te quedas bastante decepcionado. Al menos yo me quedé pensando: pero qué tipo de lenguaje de mierda es este.

Ten en cuenta lo que decía más arriba: Python se creó para ser simple, en todos los aspectos. Eso no tiene por qué ser malo. Depende de lo que necesitemos. Por ejemplo, a mí no se me ocurriría escribir un sistema operativo ni el sistema de control de un misil en Python.

A pesar del GIL, con Python se puede hacer la mayor parte de lo que se te pase por la cabeza. No hay que preocuparse.

Sobreviviendo al GIL

Vale, ya sabemos que Python no tiene hilos reales. Entonces, ¿qué hacemos si necesitamos más rendimiento? Pues tenemos muchas soluciones:

  • Multiprocessing.
  • Hilos ligeros.
  • Corrutinas.
  • Procesamiento distribuido.
  • Técnicas JIT (Just In Time).
  • Compilar todo o parte de nuestro código Python.
  • Usar otros intérpretes de Python “no oficiales”.
  • Usar C para las partes críticas, con un wrapper en Python.
  • Transformar la entrada/salida en orientada a eventos.
  • Etcétera.

Opciones hay. Todo depende del tipo de problema que tengamos delante: uso intenso de red, carga de cómputo…

En el curso estudiaremos los puntos en negrita. Sí, es intenso. Pero no te preocupes, que bien explicado no es tan difícil.

La aproximación más fácil, sobre todo a la hora de entender el concepto, es la primera: multiprocessing.

Es tan sencillo como que, en lugar de crear hilos, creamos procesos. Ya está. Los procesos se ejecutan en cores distintos del procesador, y con eso conseguimos paralelismo real.

Ojo, que esto no es la panacea. Tiene muchos problemas y no vale para todo. En el curso veremos sus pros y sus contras.

Por último: un caso curioso

Como los hilos en Python no existen tal y como los entendemos en otros lenguajes, pasan cosas curiosas. Por ejemplo esta: cuando la ejecución con hilos es más lenta que la ejecución sin hilos.

Tomemos el siguiente ejemplo: un productor que genera números del 1 al 1.000.000 y uno o varios consumidores que operan con esos números.

El código sin hilos:

def countdown(n):
    while n > 0:
        n -= 1

def main():
    countdown(100000000)

if __name__ == '__main__':
    main()

El código con hilos. Repartimos el trabajo entre cuatro hilos:

from threading import Thread

def countdown(n):
    while n > 0:
        n -= 1

def main():

    COUNT = 100000000

    t1 = Thread(target=countdown, args=(COUNT//4, ))
    t2 = Thread(target=countdown, args=(COUNT//4, ))
    t3 = Thread(target=countdown, args=(COUNT//4, ))
    t4 = Thread(target=countdown, args=(COUNT//4, ))

    t1.start(); t2.start(); t3.start(); t4.start()

    t1.join(); t2.join(); t3.join(); t4.join()

if __name__ == '__main__':
    main()

Si ejecutamos cada uno, salen estos tiempos:

# python count.py
real    0m6.820s
user    0m6.585s
sys     0m0.079s

# python threads-count.py
real    0m15.583s
user    0m10.851s
sys     0m14.071s

Ostras. La ejecución sin hilos ha sido más rápida. ¿Por qué? Sin entrar en muchos detalles (lo dejo para otro post), lo que ha pasado es esto:

  1. Python simula la concurrencia, como ya sabemos.
  2. Python va intercambiando la ejecución de los hilos para que solo corra uno a la vez.
  3. Ese cambio de contexto, en determinadas situaciones, cuesta más que el trabajo que va a hacer el propio hilo.
  4. Por lo tanto, la solución con un solo hilo es más rápida: no gasta tiempo en cambios de contexto, y el trabajo que hace cada hilo cuesta menos que el propio cambio.

Para los expertos en Python que leáis esto: sé que me dejo muchos detalles y que es una explicación muy simplificada.

Conclusión

La concurrencia y el paralelismo son complicados en cualquier lenguaje, aunque también muy interesantes. En Python, además, pueden llegar a ser un reto.

En el siguiente post contaré más cosas del mundo oscuro de la concurrencia en Python.

Nos vemos en junio en Securízame.