Threads en Python

¿Qué son los procesos y los threads?

Las computadoras serían mucho menos útiles si no pudiéramos hacer más de una cosa a la vez. Si no pudiéramos, por ejemplo, escuchar música en nuestro reproductor de audio favorito mientras leemos un tutorial de Python en Mundo Geek.

Pero, ¿cómo se conseguía esto en computadoras antiguas con un solo núcleo / una sola CPU? Lo que ocurría, y lo que ocurre ahora, es que en realidad no estamos ejecutando varios procesos a la vez (se llama proceso a un programa en ejecución), sino que los procesos se van turnando y, dada la velocidad a la que ejecutan las instrucciones, nosotros tenemos la impresión de que las tareas se ejecutan de forma paralela como si tuviéramos multitarea real.

Cada vez que un proceso distinto pasa a ejecutarse es necesario realizar lo que se llama un cambio de contexto, durante el cual se salva el estado del programa que se estaba ejecutando a memoria y se carga el estado del programa que va a entrar a ejecutarse.

En Python podemos crear nuevos procesos mediante la función os.fork, que ejecuta la llamada al sistema fork, o mediante otras funciones más avanzadas como popen2.popen2, de forma que nuestro programa pueda realizar varias tareas de forma paralela.

Sin embargo el cambio de contexto puede ser relativamente lento, y los recursos necesarios para mantener el estado demasiados, por lo que a menudo es mucho más eficaz utilizar lo que se conoce como threads, hilos de ejecución, o procesos ligeros.

Los threads son un concepto similar a los procesos: también se trata de código en ejecución. Sin embargo los threads se ejecutan dentro de un proceso, y los threads del proceso comparten recursos entre si, como la memoria, por ejemplo.

El sistema operativo necesita menos recursos para crear y gestionar los threads, y al compartir recursos, el cambio de contexto es más rápido. Además, dado que los threads comparten el mismo espacio de memoria global, es sencillo compartir información entre ellos: cualquier variable global que tengamos en nuestro programa es vista por todos los threads.

El GIL

La ejecución de los threads en Python está controlada por el GIL (Global Interpreter Lock) de forma que sólo un thread puede ejecutarse a la vez, independientemente del número de procesadores con el que cuente la máquina. Esto posibilita que el escribir extensiones en C para Python sea mucho más sencillo, pero tiene la desventaja de limitar mucho el rendimiento, por lo que a pesar de todo, en Python, en ocasiones nos puede interesar más utilizar procesos que threads, que no sufren de esta limitación.

Cada cierto número de instrucciones de bytecode la máquina virtual para la ejecución del thread y elige otro de entre los que estaban esperando.

Por defecto el cambio de thread se realiza cada 10 instrucciones de bytecode, aunque se puede modificar mediante la función sys.setcheckinterval. También se cambia de thread cuando el hilo se pone a dormir con time.sleep o cuando comienza una operación de entrada/salida, las cuales pueden tardar mucho en finalizar, y por lo tanto, de no realizar el cambio, tendríamos a la CPU demasiado tiempo sin trabajar esperando a que la operación de E/S terminara.

Para minimizar un poco el efecto del GIL en el rendimiento de nuestra aplicación es conveniente llamar al intérprete con el flag -O, lo que hará que se genere un bytecode optimizado con menos instrucciones, y, por lo tanto, menos cambios de contexto. También podemos plantearnos el utilizar procesos en lugar de threads, como ya comentamos, utilizando por ejemplo el módulo processing; escribir el código en el que el rendimiento sea crítico en una extensión en C o utilizar IronPython o Jython, que carecen de GIL.

Threads en Python

El trabajo con threads se lleva a cabo en Python mediante el módulo thread. Este módulo es opcional y dependiente de la plataforma, y puede ser necesario, aunque no es común, recompilar el intérprete para añadir el soporte de threads.

Además de thread, también contamos con el módulo threading que se apoya en el primero para proporcionarnos una API de más alto nivel, más completa, y orientada a objetos. El módulo threading se basa ligeramente en el modelo de threads de Java.

El módulo threading contiene una clase Thread que debemos extender para crear nuestros propios hilos de ejecución. El método run contendrá el código que queremos que ejecute el thread. Si queremos especificar nuestro propio constructor, este deberá llamar a threading.Thread.__init__(self) para inicializar el objeto correctamente.

import threading

class MiThread(threading.Thread):
      def __init__(self, num):
          threading.Thread.__init__(self)
          self.num = num

      def run(self):
          print "Soy el hilo", self.num

Para que el thread comience a ejecutar su código basta con crear una instancia de la clase que acabamos de definir y llamar a su método start. El código del hilo principal y el del que acabamos de crear se ejecutarán de forma concurrente.

print "Soy el hilo principal"

for i in range(0, 10):
    t = MiThread(i)
    t.start()
    t.join()

El método join se utiliza para que el hilo que ejecuta la llamada se bloquee hasta que finalice el thread sobre el que se llama. En este caso se utiliza para que el hilo principal no termine su ejecución antes que los hijos, lo cuál podría resultar en algunas plataformas en la terminación de los hijos antes de finalizar su ejecución. El método join puede tomar como parámetro un número en coma flotante indicando el número máximo de segundos a esperar.

Si se intenta llamar al método start para una instancia que ya se está ejecutando, obtendremos una excepción.

La forma recomendada de crear nuevos hilos de ejecución consiste en extender la clase Thread, como hemos visto, aunque también es posible crear una instancia de Thread directamente, e indicar como parámetros del constructor una clase ejecutable (una clase con el método especial __call__) o una función a ejecutar, y los argumentos en una tupla (parámetro args) o un diccionario (parámetro kwargs).

import threading

def imprime(num):
    print "Soy el hilo", num

print "Soy el hilo principal"

for i in range(0, 10):
    t = threading.Thread(target=imprime, args=(i, ))
    t.start()

Además de los parámetros target, args y kwargs también podemos pasar al constructor un parámetro de tipo cadena name con el nombre que queremos que tome el thread (el thread tendrá un nombre predeterminado aunque no lo especifiquemos); un parámetro de tipo booleano verbose para indicar al módulo que imprima mensajes sobre el estado de los threads para la depuración y un parámetro group, que por ahora no admite ningún valor pero que en el futuro se utilizará para crear grupos de threads y poder trabajar a nivel de grupos.

Para comprobar si un thread sigue ejecutándose, se puede utilizar el método isAlive. También podemos asignar un nombre al hilo y consultar su nombre con los métodos setName y getName.

Mediante la función threading.enumerate obtendremos una lista de los objetos Thread que se están ejecutando, incluyendo el hilo principal (podemos comparar el objeto Thread con la variable main_thread para comprobar si se trata del hilo principal) y con threading.activeCount podemos consultar el número de threads ejecutándose.

Los objetos Thread también cuentan con un método setDaemon que toma un valor booleano indicando si se trata de un demonio. La utilidad de esto es que si solo quedan threads de tipo demonio ejecutándose, la aplicación terminará automáticamente, terminando estos threads de forma segura.

Por último tenemos en el módulo threading una clase Timer que hereda de Thread y cuya utilidad es la de ejecutar el código de su método run después de un periodo de tiempo indicado como parámetro en su constructor. También incluye un método cancel mediante el que cancelar la ejecución antes de que termine el periodo de espera.

Sincronización

Uno de los mayores problemas a los que tenemos que enfrentarnos al utilizar threads es la necesidad de sincronizar el acceso a ciertos recursos por parte de los threads. Entre los mecanismos de sincronización que tenemos disponibles en el módulo threading se encuentran los locks, locks reentrantes, semáforos, condiciones y eventos.

Los locks, también llamados mutex (de mutual exclusion), cierres de exclusión mutua, cierres o candados, son objetos con dos estados posibles: adquirido o libre. Cuando un thread adquiere el candado, los demás threads que lleguen a ese punto posteriormente y pidan adquirirlo se bloquearán hasta que el thread que lo ha adquirido libere el candado, momento en el cuál podrá entrar otro thread.

El candado se representa mediante la clase Lock. Para adquirir el candado se utiliza el método acquire del objeto, al que se le puede pasar un booleano para indicar si queremos esperar a que se libere (True) o no (False). Si indicamos que no queremos esperar, el método devolverá True o False dependiendo de si se adquirió o no el candado, respectivamente. Por defecto, si no se indica nada, el hilo se bloquea indefinidamente.

Para liberar el candado una vez hemos terminado de ejecutar el bloque de código en el que pudiera producirse un problema de concurrencia, se utiliza el método release.

lista = []

lock = threading.Lock()

def anyadir(obj):
    lock.acquire()
    lista.append(obj)
    lock.release()

def obtener():
    lock.acquire()
    obj = lista.pop()
    lock.release()
    return obj

La clase RLock funciona de forma similar a Lock, pero en este caso el candado puede ser adquirido por el mismo thread varias veces, y no quedará liberado hasta que el thread llame a release tantas veces como llamó a acquire. Como en Lock, y como en todas las primitivas de sincronización que veremos a continuación, es posible indicar a acquire si queremos que se bloquee o no.

Los semáforos son otra clase de candados. La clase correspondiente, Semaphore, también cuenta con métodos acquire y release, pero se diferencia de un Lock normal en que el constructor de Semaphore puede tomar como parámetro opcional un entero value indicando el número máximo de threads que pueden acceder a la vez a la sección de código crítico. Si no se indica nada permite el acceso a un solo thread.

Cuando un thread llama a acquire, la variable que indica el número de threads que pueden adquirir el semáforo disminuye en 1, porque hemos permitido entrar en la sección de código crítico a un hilo más. Cuando un hilo llama a release, la variable aumenta en 1.

No es hasta que esta variable del semáforo es 0, que llamar a acquire producirá un bloqueo en el thread que realizó la petición, a la espera de que algún otro thread llame a release para liberar su plaza.

Es importante destacar que el valor inicial de la variable tal como lo pasamos en el constructor, no es un límite máximo, sino que múltiples llamadas a release pueden hacer que el valor de la variable sea mayor que su valor original. Si no es esto lo que queremos, podemos utilizar la clase BoundedSemaphore en cuyo caso, ahora si, se consideraría un error llamar a release demasiadas veces, y se lanzaría una excepción de tipo ValueError de superarse el valor inicial.

Podríamos utilizar los semáforos, por ejemplo, en un pequeño programa en el que múltiples threads descargaran datos de una URL, de forma que pudieramos limitar el número de conexiones a realizar al sitio web para no bombardear el sitio con cientos de peticiones concurrentes.

semaforo = threading.Semaphore(4)

def descargar(url):
    semaforo.acquire()
    urllib.urlretrieve(url)
    semaforo.release()

Las condiciones (clase Condition) son de utilidad para hacer que los threads sólo puedan entrar en la sección crítica de darse una cierta condición o evento. Para esto utilizan un Lock pasado como parámetro, o crean un objeto RLock automaticamente si no se pasa ningún parámetro al constructor.

Son especialmente adecuadas para el clásico problema de productor-consumidor. La clase cuenta con métodos acquire y release, que llamarán a los métodos correspondientes del candado asociado. También tenemos métodos wait, notify y notifyAll.

El método wait debe llamarse después de haber adquirido el candado con acquire. Este método libera el candado y bloquea al thread hasta que una llamada a notify o notifyAll en otro thread le indican que se ha cumplido la condición por la que esperaba. El thread que informa a los demás de que se ha producido la condición, también debe llamar a acquire antes de llamar a notify o notifyAll.

Al llamar a notify, se informa del evento a un solo thread, y por tanto se despierta un solo thread. Al llamar a notifyAll se despiertan todos los threads que esperaban a la condición.

Tanto el thread que notifica como los que son notificados tienen que terminar liberando el lock con release.

lista = []
cond = threading.Condition()

def consumir():
    cond.acquire()
    cond.wait()
    obj = lista.pop()
    cond.release()
    return obj

def producir(obj):
    cond.acquire()
    lista.append(obj)
    cond.notify()
    cond.release()

Los eventos, implementados mediante al clase Event, son un wrapper por encima de Condition y sirven principalmente para coordinar threads mediante señales que indican que se ha producido un evento. Los eventos nos abstraen del hecho de que estemos utilizando un Lock por debajo, por lo que carece de métodos acquire y release.

El thread que debe esperar el evento llama al método wait y se bloquea, opcionalmente pasando como parámetro un número en coma flotante indicando el número máximo de segundos a esperar. Otro hilo, cuando ocurre el evento, manda la señal a los threads bloqueados a la espera de dicho evento utilizando el método set. Los threads que estaban esperando se desbloquean una vez recibida la señal. El flag que determina si se ha producido el evento se puede volver a establecer a falso usando clear.

Como vemos los eventos son muy similares a las condiciones, a excepción de que se desbloquean todos los threads que esperaban el evento y que no tenemos que llamar a acquire y release.

import threading, time

class MiThread(threading.Thread):
      def __init__(self, evento):
          threading.Thread.__init__(self)
          self.evento = evento

      def run(self):
          print self.getName(), "esperando al evento"
          self.evento.wait()
          print self.getName(), "termina la espera"
      

evento = threading.Event()
t1 = MiThread(evento)
t1.start()
t2 = MiThread(evento)
t2.start()

# Esperamos un poco 
time.sleep(5)
evento.set()

Por último, un pequeño extra. Si sois usuarios de Java sin duda estaréis echando en falta una palabra clave syncronized para hacer que sólo un thread pueda acceder al método sobre el que se utiliza a la vez. Una construcción común es el uso de un decorador para implementar esta funcionalidad usando un Lock. Sería algo así:

def synchronized(lock):
    def dec(f):
        def func_dec(*args, **kwargs):
            lock.acquire()
            try:
                return f(*args, **kwargs)
            finally:
                lock.release()
        return func_dec
    return dec


class MyThread(threading.Thread):
    @synchronized(mi_lock)
    def run(self):
        print "metodo sincronizado"

Datos globales independientes

Como ya hemos comentado los threads comparten las variables globales. Sin embargo pueden existir situaciones en las que queramos utilizar variables globales pero que estas variables se comporten como si fueran locales a un solo thread. Es decir, que cada uno de los threads tengan valores distintos independientes, y que los cambios de un determinado thread sobre el valor no se vean reflejados en las copias de los demás threads.

Para lograr este comportamiento se puede utilizar la clase threading.local, que crea un almacén de datos locales. Primero debemos crear una instancia de la clase, o de una subclase, para después almacenar y obtener los valores a través de parámetros de la clase.

datos_locales = threading.local()
datos_locales.mi_var = "hola"
print datos_locales.mi_var

Fijémonos en el siguiente código, por ejemplo. Para el hilo principal el objeto local tiene un atributo var, y por lo tanto el print imprime su valor sin problemas. Sin embargo para el hilo t ese atributo no existe, y por lo tanto lanza una excepción.

local = threading.local()

def f():
    print local.var


local.var = "hola"
t = threading.Thread(target=f)
print local.var
t.start()
t.join()

Compartir información

Para compartir información entre los threads de forma sencilla podemos utilizar la clase Queue.Queue, que implementa una cola (una estructura de datos de tipo FIFO) con soporte multihilo. Esta clase utiliza las primitivas de threading para ahorrarnos tener que sincronizar el acceso a los datos nosotros mismos.

El constructor de Queue toma un parámetro opcional indicando el tamaño máximo de la cola. Si no se indica ningún valor no hay límite de tamaño.

Para añadir un elemento a la cola se utiliza el método put(item); para obtener el siguiente elemento, get(). Ambos métodos tienen un parámetro booleano opcional block que indica si queremos que se espere hasta que haya algún elemento en la cola para poder devolverlo o hasta que la cola deje de estar llena para poder introducirlo.

También existe un parámetro opcional timeout que indica, en segundos, el tiempo máximo a esperar. Si el timeout acaba sin poder haber realizado la operación debido a que la cola estaba llena o vacía, o bien si block era False, se lanzará una excepción de tipo Queue.Full o Queue.Empty, respectivamente.

Con qsize obtenemos el tamaño de la cola y con empty() e full() podemos comprobar si está vacía o llena.

q = Queue.Queue() 

class MiThread(threading.Thread):
    def __init__(self, q):
        self.q = q
        threading.Thread.__init__(self)

    def run(self):
        while True: 
            try:
                obj = q.get(False) 
            except Queue.Empty:
                print "Fin" 
                break
            print obj

for i in range(10):
    q.put(i)
    
t = MiThread(q)
t.start()
t.join()


Comentarios
  1. Llevo aprendiendo Python desde hace un poquito tiempo, por entretenerme más que nada y me estaba gustando bastante.
    Pero esto que cuentas de los hilos me deja perplejo perplejito – que diría Flanders -. ¿Qué sentido tiene permitir que únicamente se ejecute un hilo? Teniendo en cuenta que hoy en día empiezan a abundar las máquinas con varios núcleos creo que es una patada en la boca. Supongo que habrá alguna razón de peso para que esto sea así, pero es que no se me ocurre ninguna, porque que para poder aprovechar los procesadores de varios núcleos haya que utilizar procesos en lugar de hilos no me parece muy normal. ¿Por curiosidad no sabrás a qué se debe no?
    Saludos.

    Responder

  2. Pues es algo que Guido responde cada cierto tiempo. De hecho es una de las cosas que responde en el FAQ de Python 3000.

    Lo primero es que al menos el GIL se libera antes de operaciones de E/S o cuando el proceso se pone a dormir, que es una de las situaciones donde más tiempo perderías. Luego en programas normales las mejoras en tiempo tampoco serían tan absurdamente enormes.

    Luego, que Python tampoco está enfocado a aplicaciones en las que el rendimiento sea crítico. En todo caso mezclarías C y Python, y ya no tendrías problemas con el GIL.

    Por otro lado consideran que tienes suficientes alternativas. Y que los threads tampoco son algo a alentar. Otras sugerencias: Parallel Processing and Multiprocessing in Python

    Además hace unos años escribieron una versión de Python que no usaba el GIL y mientras que las mejoras de rendimiento en varios núcleos no eran espectaculares, para máquinas de un solo núcleo se empeoraba bastante.

    Por cierto, Ruby también usa algo parecido al GIL, ahora que lo pienso…

    Aquí tienes una de estas discusiones, por si te interesa profundizar un poco más: It isn’t Easy to Remove the GIL

    Evidentemente a mí lo que me gustaría es una implementación que sacara todo el partido de los multi núcleo sin perder mucho rendimiento en mono núcleo. Pero bueno, es de las pocas cosas malas de Python que me vienen a la cabeza y tampoco es algo que me afecte mucho.

    Responder

  3. yiTaN!

    Zootropo, lo cierto es que soy un poco ‘trozo’ en Python y he estado aprendiendo gracias a tus tutoriales, aunque esté bastante verde.

    Aún así, quiero darte las gracias especialmente por esta entrada porque, justamente, estoy dando esto ahora mismo en la uni (de hecho, el próximo martes tengo el exámen) y me sirve para practicar 🙂

    Lo dicho, muchas gracias por estos tutoriales. Son buenísimos 😉

    Responder

  4. ¿El examen es de threads en Python, yiTaN!? ¿o de procesos y threads en un curso de sistemas operativos?

    Responder

  5. Sigo con atención tus apuntes de Python, así que van aquí una serie de matizaciones sobre los “Locks”, “Semáforos” y “cerrojos”.

    Lock y RLock en Python *son* un subconjunto de los semáforos (binarios) que sirven fundamentalmente para exclusión mutua. El nombre correcto sería “semáforos binarios”, o “semáforos mutex” o “mutex” para ser simples.

    La abstracción de semáforo fue inventada por Dijkstra en 1974 y si no recuerdo mal fueron inicialmente binarios, luego extendidos a los semáforos contadores (son semánticamente equivalentes). De allí que los “semáforos” sean generalmente contadores a diferencia de los binarios más simples generalmente llamados “mutex” (y que se pueden implementar de forma más eficiente).

    Se suele usar la traducción “cerrojo” a los “spinlocks”. Pero estos son bestias diferentes y a diferencia de los semáforos/mutex sí tienen espera activa (normalmente implementado con instrucciones específicas de hardware como el test&set o swap con el objetivo de asegurar atomicidad, especialmente en multiprocesadores).

    Otra matización:

    > Para minimizar un poco el efecto del GIL en el rendimiento de nuestra aplicación es conveniente llamar al intérprete con el flag -O, lo que hará que se genere un bytecode optimizado con menos instrucciones, y, por lo tanto, menos cambios de contexto.

    En realidad no se produce cambio de contexto, todos los threads se ejecutan en el mismo contexto que el intérprete Python. La optimización viene por una reducción del “overhead” al ejecutarse más operaciones “efectivas” entre cada verificación.

    PS: ¿cuándo podrás disponible todos los apuntes de Python en un sólo PDF o HTML? Así se los paso a mis alumnos 🙂

    Responder

  6. Gracias por las notas Ricardo, siempre es un placer leerte 🙂

    Antes de crear un PDF o un HTML tengo que pensar un título, y decidir si usar Open Office.org, Word, LaTeX o directamente un programa de maquetado como Scribus o Quark Xpress 😛

    Responder

  7. De nuevo, mil gracias.
    Avanzo más con tus tutoriales que con lo que doy en clase de Python… 😀

    Responder

  8. yiTaN!

    @ Zootropo: Perdona por no responderte antes. No, el examen no es en Python. Es realmente de concurrencia, y problemas de sincronización entre procesos e hilos ejecutados concurrentemente.

    Lo que digo es que con esto puedo practicar más que con pseudocódigo feo 😛

    Responder

  9. Zootropo, yo lo haría fácil. Un c&p de los artículos que hayas escrito en OOo, pones un título provisional, por ejemplo “Python en MundoGeek” y cada vez que agregas un apunte nuevo lo vas agregando como capítulo y generas un PDF.

    Así puedes poner en cada apunte: “bajarse el PDF”. Luego, cuando tengas ganas lo mejoras y publicas en Lulu o Bubok 🙂

    yiTaN, prueba con las Ada Tasks y disfruta 🙂

    Responder

  10. Julius

    Zoo!! Una pregunta… que fuente es esa y que editor de texto usas? Me recuerda a consolas pero nunca he conseguido que se viera así de bien esa fuente en ubuntu…

    Saludos!!

    Responder

  11. Julius

    esto….
    acabo de darme cuenta de que no son screenshots jajaja

    ignora el comentario anterior

    :blush:

    Responder

  12. Buenas,

    Julius, usa geany, es el que uso y es bastante bueno

    salu2

    Responder

  13. David Esquivel

    Que buen tutorial. Muchas gracias.

    Responder

  14. […] virtual de Python y acabando con la necesidad de limitar la ejecución de los threads a través del GIL (Global Interpreter Lock), entre […]

    Responder

  15. Hola

    Esto de los hilos es interesante, pero aquí hablas del módulo threading, por que no hablar del módulo a más bajo nivel que es thread y hablar de como usarlo, ventajas, desventajas, y como no, bien explicado como lo haces siempre.
    Ya sé que para eso está este módulo, pero nunca hace daño saber algo demás

    Saludos!

    Responder

  16. […] /me leyendo sobre threads en python 😀 http://mundogeek.net/archivos/2008/04/18/threads-en-python/ […]

    Responder

  17. leonel

    Estoy buscando la forma de poner mi programa en el la barra de tareas, como cuando uno minimiza el jdownloader. Yo estoy utilizando tkinter pero ya llevo varios dias buscando la forma, y no la encuentro.
    Si alguno conoce el nombre del metodo que me sirve, y un script de ejemplo de como se usa en tkinter. Favor enviarmelo a este correo leoan91@hotmail.com.
    De paso le agradezco su colaboración, y este comentario lo escribi aqui porque no encontre un lugar más apropiado para hacerlo.
    Gracias.

    Responder

  18. Francisco Pozo

    Muy útiles me han parecido. Tenía un problema en python justamente con este tema y me lo han resuelto entero.
    Muchas gracias por este aporte.

    Responder

  19. Muchas gracias por este pedazo de artículo! Todo está explicado perfectamente. Se entiende a la primera todo lo que quieres decir. Muuuchas gracias, de verdad!

    Responder

  20. Muy buen articulo, me ayudo mucho.

    Gracias.

    Responder

  21. Julio A.

    Muy bueno!!! Me ayudo un monton!

    Responder

  22. Alberto

    Hola soy nuevo en esto de la programación pero soy un usuario muy avanzado de la computacion, quisiera saber que programa se debe utilizar para poder practicar este lenguaje en win7 y en ubuntu y cual es mas conveniente practicar java, c, o este espero me puedan orientar.

    Responder

  23. nbensa

    Hola.

    En el método run del último ejemplo, la línea:

    obj = q.get(False)

    no debería ser:

    obj = self.q.get(False)

    ?

    Gracias.

    Responder

    • Caldron

      nbensa,

      estás en lo cierto, lo correcto es:

      obj = self.q.get(False)

      Responder

  24. oSHINSAo

    Estuve desarrollando un sitio, el cual, al realizar una actualizacion o dar de alta un “caso” notificara a los encargados del mismo, el cual supondria TIEMPO al procesar el envio de correos a todos mis recipientes.

    Todos conocemos los coneptos o los hemos oido en algun lugar, ejecutar procesos en Paralelo o Background.

    entonces realice mi investigacion, y descubri como usarlos, ahora cada que hago una actualizacion, mi pagina se REFRESCA INSTANTANEAMENTE con django, mientras mi proceso se encarga de tomar el tiempo de enviar los correos y que no me afecte en mi produccion.

    Saludos y suerte a todos

    Responder

  25. Diego

    en tanto que queue.set(a, True) seria el metodo sincrono de apilar queues y queue.set(a, False) el metodo asincronico. suerte.

    Responder

  26. Diana_FM

    Hola.

    Tengo un programa en python y en un bucle estoy usando la función time.sleep(). El problema que tengo es que con esta funcion, la interfaz de mi programa se queda pegada y no tengo acceso a ella ni a hacer modificaciones en ella.

    Alguna idea?

    Agradezco su colaboración.

    Responder

  27. Diana_FM

    Hola.

    Estoy haciendo un programa en python con la funcion time.sleep(). El problema que tengo es que al hacer uso de esta funcion la interfaz se pega y aunque el programa se este ejecutando no puedo acceder a la interfaz.

    Alguna recomendacion?

    Responder

  28. Rene

    Hola, amigos, estoy trabajando en un proyecto de Pygame, me gustaría que me dieran una orientación de como podría integrar threading en el mismo, desde ya mis saludos, muy buen tutorial

    Responder

Deja un comentario