Introducción a Stackless Python



Stackless es una implementación de Python desarrollada por Christian Tismer con el objetivo de brindar al programador la posibilidad de aprovechar los beneficios de la programación basada en hilos (threads).

CPython, la implementación original, incluye un bloqueo generalizado (GIL) que evita que varios hilos ejecuten código Python simultáneamente. Stackless permite la utilización de microthreads (también llamados tasklets) que son, principalmente, hilos livianos que consumen menos memoria que los convencionales; por lo tanto, puede alcanzarse mayor simultaneidad en relación con los hilos del sistema.

Originalmente, Stackless (el cual lanzó su primera versión en 1998) se distribuía como un paquete convencional, y se accedía a sus funcionalidades a través del módulo stackless. Años más tarde, y hasta el día de hoy, se distribuye como un intérprete separado del original CPython.

Instalación

En la sección de descargas del sitio oficial podrás encontrar las distintas opciones para obtener Stackless e instalarlo.

En sistemas basados en Linux, puedes compilar el código directamente desde el repositorio de Mercurial o bien descargarlo como un archivo comprimido.

Para Mac OS X y Microsoft Windows, Stackless provee instaladores que reemplazarán tu versión existente de Python. Por ejemplo, si cuentas con una instalación de CPython 2.7.6, considera utilizar otra versión de Stackless o bien especificar un directorio de instalación alternativo.

Además, puedes utilizar pip o easy_install:

pip install stackless-python

easy_install stackless-python

Conceptos básicos

Una vez instalado Stackless, ejecuta el intérprete y verás un mensaje similar al siguiente.

Python 2.7.8 Stackless 3.1b3 060516 (default, Jul 11 2014, 20:03:24) [MSC v.1500
64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>

Puedes acceder a todas sus funcionalidades importando el módulo stackless.

import stackless

Antes de comenzar a realizar algunas tareas simultáneas es importante tener en claro tres conceptos principales:

  • Tasklets o microthreads: hilos livianos, concepto principal de Stackless. Difieren de los hilos convencionales ya que consumen menos memoria y están exentos de la pila de C (C Stack, más información).
  • Scheduler: podría traducirse al castellano como «planificador» o «controlador». Es el encargado de administrar cada una de las tasklets: orden de ejecución, alternación, entre otras. El programador hace uso del scheduler para añadir o quitar tasklets.
  • Channels: permite comunicarse de forma segura entre las distintas tareas.

Tasklets

Puedes crear una tasklet indicando el nombre de una función junto con determinados argumentos, si es que requiere alguno.

stackless.tasklet(func)(arg1, arg2, kwarg1=1)

El código anterior retorna una instancia de la clase tasklet, por lo que generalmente es conveniente mantener una referencia a la reciente creada tarea.

Existen otros métodos para la creación de tareas que resultan similares.

t = stackless.tasklet()
t.bind(func)
t.setup(arg1, arg2, kwarg1=1)

A continuación un código funcional que ejecuta una función en una tasklet la cual imprime un mensaje en pantalla.

>>> import stackless
>>>
>>> def func():
...     print("Hello, Stackless!")
...
>>> t = stackless.tasklet(func)()
>>> stackless.run()
Hello, Stackless!

Nótese el uso de stackless.run(). Una vez llamada, Stackless comienza a trabajar en la ejecución de cada una de las tareas. La función no retorná hasta que la ejecución de cada una de ellas haya finalizado, y éstas no comenzarán a ejecutarse hasta que se haya llamado a dicha función.

from time import time

def func():
    while True:
        print("Hello, Stackless!")
        sleep(5)

t = stackless.tasklet(func)()
stackless.run()
print("Terminado")

En este caso, func() se ejecuta infinitamente y, por lo tanto, stackless.run() bloquea la ejecución del hilo principal. Es decir, el mensaje «Terminado» nunca llega a imprimirse en pantalla.

El hilo (o tarea) principal (desde donde es llamada la función stackless.run) es generalmente aquel que ejecuta el planificador. A esta tarea se la domina main tasklet (tarea principal). Current tasklet (tarea actual) se denomina a aquella que está siendo ejecutada actualmente.

Los atributos stackless.main y stackless.current son utilizados para acceder a las instancias (aquellas retornadas por stackless.tasklet) de la tarea principal y la actual, respectivamente. Para determinar si una determinada tarea es la principal o la actual, también pueden utilizarse tasklet.is_main y tasklet.is_current. Siendo esto así, stackless.current.is_current siempre es verdadero, mientras que stackless.current.is_main es falso cuando no sea accedido desde el hilo principal.

def func():
    # Siempre verdadero.
    print(stackless.current.is_current)
    # Siempre Falso.
    print(stackless.current.is_main)

stackless.tasklet(func)()
stackless.run()

Scheduler

Las tasklets están almacenadas en una cola de funciones que es utilizada por el planificador. Cuando se llama a stackless.tasklet(func)(), la función func es añadida a la cola interna del scheduler, y retirada automáticamente cuando finaliza su ejecución. Para determinar cuántas tareas se encuentran en la cola, se utiliza el atributo stackless.runcount. A modo de ejemplo, considera el siguiente código:

def func():
    print(stackless.runcount)

stackless.tasklet(func)()
stackless.tasklet(func)()
stackless.tasklet(func)()
stackless.run()

Imprime en pantalla:

3
2
1

Como puede observarse, el planificador remueve las tareas automáticamente cuando la función a ejecutar finaliza.

Sin embargo, en algunas ocasiones es necesario indicarle al planificador que nuestra función ya se ha ejecutado, para darle lugar a otras tareas que se encuentren en la cola. Considera el siguiente código:

import stackless
from time import sleep

def func(n):
    while True:
        print("[{0}] Hello, Stackless!".format(n))
        sleep(.5)

stackless.tasklet(func)(1)
stackless.tasklet(func)(2)
stackless.tasklet(func)(3)
stackless.run()

Imprime en pantalla:

[1] Hello, Stackless!
[1] Hello, Stackless!
[1] Hello, Stackless!
[1] Hello, Stackless!
...

¿Qué es lo que ha sucedido? Hemos creado tres tareas, pero únicamente se ejecuta la primera. La respuesta es que el planificador alterna entre las tasklets cuando la actual (stackless.current) finaliza. Como func(1) es la primera en ejecutarse y entra en un bucle infinito, el scheduler nunca llega a darle lugar a func(2) y func(3). Para solucionar esto, debemos llamar a stackless.schedule() en cada una de las tareas, que cede el foco de ejecución a la siguiente función en la cola.

def func(n):
    while True:
        print("[{0}] Hello, Stackless!".format(n))
        sleep(.5)
        stackless.schedule()

De esta manera obtenemos el resultado deseado.

[1] Hello, Stackless!
[2] Hello, Stackless!
[3] Hello, Stackless!
[1] Hello, Stackless!
[2] Hello, Stackless!
[3] Hello, Stackless!
[1] Hello, Stackless!
[2] Hello, Stackless!
[3] Hello, Stackless!

Al llamar a stackless.run sin argumentos, se establece la planificación cooperativa, en donde cada tarea es responsable de ceder el foco para permitir a otras ejecutarse, generalmente llamado a stackless.schedule(). Existen otros tipos de planificación, menos convencionales, que puedes conocer en la documentación oficial.

Por último, y para finalizar con el planificador, pueden crearse tasklets durante la ejecución del programa, luego de haber llamado a stackless.run(), utilizando la función tasklet.insert, que inserta una tarea al final de la cola (mientras no esté bloqueada). A modo de ejemplo, véase el siguiente código de un servidor TCP en donde se crea una tarea cada vez que se recibe una conexión.

#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
#      tcpserver.py
#
#      Copyright 2014 Recursos Python - www.recursospython.com
#
#

import stackless
import stacklesssocket; stacklesssocket.install()

from socket import socket, error as socket_error

def run_socket(conn, name):
    print("%s conectado." % name)
    while True:
        try:
            # Recibir datos del cliente.
            input_data = conn.recv(1024)
        except socket_error as e:
            print("[%s] %s" % (name, e))
            break
        else:
            if len(input_data) == 0:
                break
            # Reenviar la información recibida.
            elif input_data:
                conn.send(input_data)
    print("%s desconectado." % name)


def listener():
    s = socket()
    
    # Escuchar peticiones en el puerto 6030.
    s.bind(("localhost", 6030))
    s.listen(0)
    
    while True:
        conn, addr = s.accept()
        # Crear nueva tarea y recibir datos desde allí.
        new_tasklet = stackless.tasklet(run_socket)(conn, "%s:%d" % addr)
        # Insertarla en la cola.
        new_tasklet.insert()
        # Ceder el foco de ejecución a otras tareas.
        stackless.schedule()


def main():
    # Tarea principal.
    stackless.tasklet(listener)()
    stackless.run()


if __name__ == "__main__":
    main()

Channels

Los channels son, como el nombre lo indica, canales o vías por las cuales pueden comunicarse de forma segura las distintas tasklets. A través de estos puede enviarse cualquier tipo de dato: una cadena, un entero, tuplas, listas, instancias, etc. Resultan muy fáciles de utilizar. Véase el siguiente código de ejemplo.

import stackless

def receiver(channel):
    data = channel.receive()
    print("Recibido: {0}.".format(data))

def sender(channel):
    channel.send("Hello, Stackless")

channel = stackless.channel()
stackless.tasklet(receiver)(channel)
stackless.tasklet(sender)(channel)
stackless.run()

Imprime en pantalla:

Recibido: Hello, Stackless.

Se crea una instancia de la clase channel (línea número 10) la cual se pasa como argumento a ambas tasklets y es utilizada para comunicarse entre sí. Luego, se añaden a la cola de ejecución las tareas receiver() y sender(), las cuales se ejecutan en el mismo orden al llamar a stackless.run().

channel.send() permite enviar un determinado dato a través del canal que será recibido por otra tarea que llame a la función channel.receive(). De ser esto último así, el planificador detiene la ejecución de la tarea actual y la inserta al final de la cola, y aquella que reciba los datos es resumida. En caso de llamar a channel.send() sin que otra tarea reciba los datos, al finalizar su ejecución será retirada de la cola.

De forma análoga, channel.receive() permite recibir un dato enviado por otra tarea haciendo uso de la función anterior. De ser esto último así, la tarea que llame a channel.send() es resumida y la actual es insertada al final de la cola. Si channel.receive() es llamada sin que ninguna de las otras tareas envíe datos, la actual será retirada de la cola al finalizar su ejecución.

Para demostrar este comportamiento, considera añadir algunos mensajes en las funciones del código anterior.

def receiver(channel):
    print("Recibiendo...")
    data = channel.receive()
    print("Recibido: {0}.".format(data))

def sender(channel):
    print("Enviando...")
    channel.send("Hello, Stackless")
    print("Enviado.")

La salida en pantalla es:

Recibiendo...
Enviando...
Recibido: Hello, Stackless.
Enviado.

Puede observarse cómo el planificador suspende la ejecución de receiver() cuando ésta llama a channel.receive(), y cede el foco a la función sender(). Una vez que esta última envía un mensaje a través del canal, la ejecución de receiver() es resumida y retirada de la cola al finalizar. Por último, sender() finaliza su ejecución y el programa termina al haber quedado vacía la cola del planificador.



Deja una respuesta