Optimización de desarrollos web: Threads

No es nada nuevo que la velocidad de una aplicación web es uno de los aspectos principales a tener en cuenta durante el desarrollo y, al contrario de lo que muchos piensan, no todo depende de tener el proyecto alojado en una máquina más potente. Lo que va a marcar la diferencia es la velocidad percibida por el usuario del servicio.
Como ejemplo ilustrativo, imaginad un aplicación que tras un determinado evento necesita informar del mismo a 200 usuarios vía mail. Como sabemos, el envío de un mail puede ser una tarea cara en términos de tiempo, sobre todo si se realiza a través de un servidor remoto.
Pues bien, si la aplicación envía esos 200 mails antes de devolver una respuesta al usuario y la espera se hace prolongada (por problemas de red, servidor saturado…), éste puede pensar que ha habido un error e intentará repetir la operación o, en el peor de los casos, abandonará el sitio.
Un detalle que he observado en muchos desarrollos, y que siempre me sorprende, es lo poco que se aprovechan los threads (hilos) en las aplicaciones web. Su uso es prácticamente nulo. De hecho, muchos programadores piensan que su uso está reservado exclusivamente a los desarrollos clásicos “de escritorio”. Nada más lejos de la realidad.
Threads en Django
En Python resulta extremadamente sencillo crear threads, lo cual es extensible a los desarrollos en Django. Así que, ¡vamos a aprovechar un poco esos procesadores!
Con el fin de facilitar el uso de threads aún más, vamos a crear un pequeño módulo para “inyectar” esta posibilidad en las funciones de manera inmediata y transparente.
# ---- threadhelp.py ----
import threading
import functools
def threaded (func):
def thread_call (*args, **kwargs):
t = threading.Thread (target=func, args=args, kwargs=kwargs)
t.setDaemon (True)
t.start ()
functools.update_wrapper (thread_call, func)
return thread_call
Así, aplicando el decorador @threaded sobre una función, puedo hacer que una llamada a la misma se ejecute automáticamente en un nuevo thread que devolverá el control automáticamente y permanecerá ejecutándose en segundo plano (ya que hemos configurado el thread como demonio).
A modo de ejemplo, aquí os muestro una versión simplificada de la función que utilizo para el envío de las suscripciones a los comentarios del blog.
import threadhelp
@threaded
def send_comment_mail (comment):
ctx = Context ({
'site': CURRENT_SITE,
'post': comment.post,
'comment': comment,
})
subject = get_template ('blog/mail/new_comment_subject.txt').render (ctx)
from_addr = BLOG_FROM_ADDR
t = get_template ('blog/mail/new_comment_body.txt')
for s in comment.post.commentsubscription_set.filter (cancelled=False).all ():
ctx['subscription'] = s
msg = t.render (ctx)
send_mail (subject, msg, from_addr, [s.email], fail_silently=True)
Gracas al decorador @threaded, cuando realizo una llamada a la función send_comment_mail(), ésta automáticamente se lanzará en un hilo de ejecución independiente. Es el servidor el que termina el proceso en segundo plano y sin necesidad de que el usuario deba esperar hasta el final, con lo que puedo generar y enviar la página de respuesta de manera inmediata. Ya sabéis: velocidad percibida.
Además de lo anterior, también podemos obtener versiones “rápidas” de las funciones existentes. Por ejemplo, si queremos tener una versión que se ejecute en segundo plano de la función send_mail() proporcionada por Django, no tenemos más que hacer lo siguiente:
thread_send_mail = threaded (send_mail)
Automáticamente obtendremos una copia de la función que se ejecutará en segundo plano en cada llamada.
Otro paso más hacia una optimización más “fina” sería agrupar todos los hilos que se vayan creando en una cola administrada con el fin de no lanzar miles de ellos de manera indiscriminada en caso de que el servicio esté muy concurrido. Pero eso lo dejo como ejercicio
En fín. Como suele ocurrir casi siempre, la solución a muchos problemas de rendimiento pasa por aprovechar mejor los recursos que tenemos.
Ya lo dijo Kristian Pielhoff. Fácil, sencillo y para toda la familia