martes, 1 de septiembre de 2015

Practica No.4 Programación de Hilos en Python

¿QUÉ SON LOS THREADS?

Para que una computadora sea útil es necesario que podamos hacer más de una cosa a la vez. La cuestión es cómo lograr esto en computadoras con una sola CPU, la solución fue que en realidad no ejecutar varios procesos a la vez, 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, sin embargo esto puede ser relativamente lento, por lo que a veces 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 sí. El sistema operativo necesita menos recursos para crear y gestionar los threads, y 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.


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.


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.

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, también contamos con el módulo threading que se apoya en el primero para proporcionarnos una API orientada a objetos.

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.


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.


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. 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. Podemos observar estos elementos en el siguiente ejemplo:
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)


print ("Soy el hilo principal") 
  
for i in range(0, 10):  
    t = MiThread(i)  
    t.start()  
    t.join() 

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 siguientes:


Candado


Los locks, también llamados mutex son objetos con dos estados posibles: adquirido o libre. Cuando un thread adquiere el candado, los demás threads que pidan adquirirlo se bloquearán hasta que el thread que lo ha adquirido libere el candado, momento en el cual podrá entrar otro thread.


El candado se representa mediante la clase Lock. Llamando a Lock.acquire() el hilo bloqueará el Lock, de forma que el siguiente hilo que llame a Lock.acquire() se quedará a la espera de que el Lock se desbloquee. La llamada a Lock.release() desbloquea el Lock, haciendo que el hilo que estaba en espera continúe. Un ejemplo se muestra a continuación:
from threading import Thread, Lock
import time

class MiHilo(Thread):

    
    def __init__ (self, inicio, fin, bloqueo):
        Thread.__init__(self)
        self.inicio = inicio
        self.fin = fin
        self.bloqueo = bloqueo
      
    def run (self):
        bloqueo.acquire()
        for i in range(self.inicio,self.fin):
           print ("contador = "+str(i))
           time.sleep(0.2)
        bloqueo.release()
            
if __name__ == '__main__':
    bloqueo = Lock()
    bloqueo.acquire()
    hilo = MiHilo(0,10, bloqueo)
    hilo.start()
    time.sleep(1)
    for i in range (10,20):
       print ("main = "+str(i))
       time.sleep(0.1)
       
    bloqueo.release()

Esta clase Lock es muy simple. Cualquier hilo, puede liberar a otro sin importar si lo haya bloquedado o no, además si un hilo llama él mismo dos veces a acquire(), se queda bloqueado en la segunda llamada. Una mejor opción es RLock. En este candado solo el que haya llamado al método acquire() puede liberarlo con el método release() y un mismo hilo puede llamar varias veces a acquire() sin quedarse bloqueado, pero tiene que hacer el mismo número de llamadas a release() para desbloquearlo. El ejemplo siguiente muestra su funcionamiento:

from threading import Thread, RLock
import time

class MiHilo(Thread):
    def __init__ (self, inicio, fin, bloqueo):
        Thread.__init__(self)
        self.inicio = inicio
        self.fin = fin
        self.bloqueo = bloqueo

    def run (self):
        bloqueo.acquire()
        for i in range(self.inicio,self.fin):
           print ("contador = "+str(i))
           time.sleep(0.2)
        bloqueo.release()
            
if __name__ == '__main__':
    bloqueo = RLock()
    bloqueo.acquire()
    
    hilo = MiHilo(0,10, bloqueo)
    hilo.start()
    time.sleep(1)
    for i in range (10,20):
       print ("main = "+str(i))
       bloqueo.acquire()
       time.sleep(0.1)
       
    bloqueo.release()
    print ("El hilo todavia no ha comenzado")
    for i in range(10,20):
       bloqueo.release()

Semáforos


Un semáforo permite acceder a un determinado recurso a un número máximo de hilos simultáneamente. Si hay más hilos que el máximo permitido, los pone en espera y los va dejando pasar según van terminando los que están activos. Un semáforo actúa como un contador con un valor inicial. Cada vez que un hilo llama a Semaphore.acquire(), el contador se decrementa en 1 y se deja pasar al hilo. En el momento que el contador se hace cero, NO se deja pasar al siguiente hilo que llame a acquire(), sino que lo deja bloqueado. Cada vez que se llama a Semaphore.release(), el contador se incrementa en 1. Si se hace igual a cero, libera al siguiente hilo en la cola de espera. El ejemplo de su funcionamiento se muestra a continuación: 
from threading import Thread, Semaphore
import time, random


class MiHilo(Thread):
    def __init__(self, numero_hilo, semaforo):
        Thread.__init__(self)
        self.semaforo=semaforo
        self.numero_hilo = numero_hilo
        
    def run(self):
        semaforo.acquire()
        print ("Entra hilo "+str(self.numero_hilo))
        time.sleep(random.randrange(1,10,1))
        print ("Fin hilo " + str(self.numero_hilo))
        semaforo.release()          
        
if __name__ == '__main__':
    random.seed()
    semaforo = Semaphore(5)
    for i in range(0,10):
       hilo=MiHilo(i,semaforo)
       hilo.start()
       print ("Arrancado hilo "+str(i))


Condiciones


Algo muy común es tener un hilo esperando por unos datos para tratarlos. Otro hilo es el encargado de proporcionar esos datos y avisar al primer hilo de que ya están disponibles. Para facilitar este tipo de uso tenemos la clase threading.Condition.
En primer lugar, creamos la Condition. El hilo que debe esperar por los datos, debe llamar al método Condition.acquire() y luego al Condition.wait(). Para llamar a wait() es obligatorio ser el propietario de la Condition, cosa que se consigue llamando a acquire(). La llamada a wait() libera la Condition, pero deja al hilo bloqueado hasta que alguien llame a Condition.notify().
El hilo encargado de suministrar los datos, debe llamar a Condition.acquire() para hacerse dueño de la Condition y cuando los datos estén disponibles, llamar a Condition.notify() y luego aCondition.release(). Estas dos llamadas juntas despertarán al hilo a la espera de datos. La llamada a notify() no libera la Condition, por lo que el hilo que está en el wait() será notifiado, pero no comenzará su ejecución hasta que se llame a release(). El ejemplo se muestra enseguida:
from threading import Thread, Condition
import time

class MiHilo (Thread):
    
    def __init__(self,lista,condicion):
        Thread.__init__(self)
        self.lista = lista
        self.condicion = condicion
        self.fin=False
        
    def run(self):
        self.condicion.acquire()
        while not self.fin:
            self.condicion.wait()
            if not self.fin:
               while len(lista)>0:
                  print (self.lista.pop(0))
        self.condicion.release()

if __name__ == '__main__':
    lista =[]
    condicion = Condition()
    hilo = MiHilo(lista, condicion)
    hilo.start()
    
    for i in range (0,10):
       condicion.acquire()
       lista.append(i)
       lista.append("numero "+str(i))
       condicion.notify()
       condicion.release()
       time.sleep(1)

    hilo.fin=True
    condicion.acquire()
    condicion.notify()
    condicion.release()

Eventos

La forma más fácil de hacer que un hilo espere a que otro hilo le avise es por medio de Event. El Event tiene un flag interno que indica si un hilo puede continuar o no. Un hilo llama al método Event.wait() y se queda bloqueado en espera hasta que el flag interno de Event se ponga a True. Otro hilo llame a Event.set() para poner el flag a True o bien a Event.clear() para ponerlo a False.

En realidad se trata de 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 ejemplo se muestra a continuación:

from threading import Thread, Event
import time

class MiHilo(Thread):
    def __init__(self, evento):
        Thread.__init__(self)
        self.evento=evento
    
    def run(self):
        self.evento.wait()
        print ("Entra hilo ")
        
if __name__ == '__main__':
    evento = Event()
    hilo=MiHilo(evento)
    hilo.start()
    time.sleep(2)
    print ("Hago evento.set()")
    evento.set()

La información y los ejemplos se obtuvieron de las siguientes fuentes:
Los ejemplos se pueden descargar del siguiente link.

Practica No.4 Programación Orientada a Objetos en Python

Conceptos Generales

La programación orientada a objetos (POO) es un paradigma de programación en el que los conceptos del mundo real relevantes para nuestro problema se modelan a través de clases y objetos, y nuestro programa consiste en una serie de interacciones entre estos objetos.

Los objetos se crean a partir de las clases, y pueden interactuar entre sí mediante mensajes. Algunos de los lenguajes que permiten la programación orientada a objetos son: Smalltalk, C++, Objective-C, C#, Java, Javascript, Python, entre otros.

El concepto básico para entender el funcionamiento de la POO son los conceptos de Clase y Objetos, los cuales podemos definirlos brevemente de la siguiente manera:

Las clases permiten agrupar en un nuevo tipo los datos y las funcionalidades asociadas a dichos datos, favoreciendo la separación entre los detalles de la implementación de las propiedades esenciales para su uso. Una clase define propiedades y comportamiento que se muestran en los entes llamados objetos (o instancias de una clase). La clase actúa como molde de un conjunto de objetos, de los que se dice que pertenecen a la clase.

En términos más abstractos, podemos pensar en las clases como definiciones y en los objetos como expresiones concretas de dichas definiciones.

Implementación en Python.

Antes que nada veamos el siguiente ejemplo
class Gato:
    def __init__(self, energia, hambre):
        self.energia = energia
        self.hambre = hambre
        print ('Se creo un gato')
 
    def tomar_leche(self, leche_en_litros):
        self.hambre += leche_en_litros
        print ('el gato toma su leche')
 
    def acariciar(self):
        print ('prrrrr...')
 
    def jugar(self):
        if self.energia <= 0 or self.hambre <=1:
            print ('el gato no quiero jugar')
        else:
            self.energia -=1
            self.hambre -= 2
            print ('al gato le encanta jugar')
 
    def dormir(self, horas):
        self.energia += horas
        print ('el gato tomo una siesta')


gato = Gato(5, 5)
gato.acariciar()
gato.jugar()
gato.jugar()
gato.jugar()
print (gato.energia)
print (gato.hambre)
gato.tomar_leche(4) 
print (gato.hambre)
gato.jugar()
print (gato.hambre)
print (gato.energia)

Como podemos ver creamos una clase a la que llamamos Gato con sus métodos y atributos propios, hay que fijarnos primero en la función _init_, como podemos observar este es el método que funciona como constructor del método y es el primero que se ejecuta al instanciar un objeto.

Aquí otra cosa a resaltar es que todos los métodos de la clase deben recibir como parámetro el atributo self, aunque nosotros al llamar a la función no lo indicamos, esto es para que cada método pueda acceder a los atributos y métodos propios de la clase. Para instanciar un objeto lo hacemos usando de la instrucción siguiente:
gato = Gato(5, 5)
Es importante pasar los parámetros que solicita la función _init_ al momento de realizar la instancia, y una vez que se realiza correctamente, ya podemos hacer referencia a los métodos y atributos de la clase usando el operador “.”.

Otro concepto importante es la herencia es decir generar una clase nueva a partir de otra, de la que recibe su comportamiento y estado (métodos y atributos), adaptándolos o ampliándolos según sea necesario. En el siguiente ejemplo vemos la clase Felino y la clase Gato que hereda de ella, esto se hace desde la declaración de la clase Gato como vemos en la siguiente sección de código:
class Felino:
    def __init__(self):
        print ('se creo el felino')
 
    def rugido(self):
        print ('El felino dio un rugido')
 
class Gato(Felino):
    def __init__(self, energia, hambre):
        self.energia = energia
        self.hambre = hambre
        print ('Se creo un gato')
 
    def tomar_leche(self, leche_en_litros):
        self.hambre += leche_en_litros
        print ('el gato toma su leche')
 
    def acariciar(self):
        print ('prrrrr...')
 
    def jugar(self):
        if self.energia <= 0 or self.hambre <=1:
            print ('el gato no quiero jugar')
        else:
            self.energia -=1
            self.hambre -= 2
            print ('al gato le encanta jugar')
 
    def dormir(self, horas):
        self.energia += horas
        print ('el gato tomo una siesta')

gato = Gato(3,3) 
gato.rugido()
De este modo el método rugido que se definió en la clase Felino puede ser accedido desde la clase Gato.

Algo que no existe en otros lenguajes de programación es el de poder heredar de múltiples clases al mismo tiempo, sin embargo esto si existe en Python, y el siguiente ejemplo es una muestra de ello, ya que nuestra clase Gato hereda tanto de la clase Felino como de la clase Mascota, por lo que puede acceder a métodos de ambas clases:
class Mascota:
    def __init__(self):
        print ('se creo la mascota')
 
    def sientate(self):
        print ('La mascota se sentó')
 
class Felino:
    def __init__(self):
        print ('se creo el felino')
 
    def rugido(self):
        print ('El felino dio un rugido')
 
class Gato(Felino, Mascota):
    def __init__(self, energia, hambre):
        self.energia = energia
        self.hambre = hambre
        print ('Se creo un gato')
 
    def tomar_leche(self, leche_en_litros):
        self.hambre += leche_en_litros
        print ('el gato toma su leche')
 
    def acariciar(self):
        print ('prrrrr...')
 
    def jugar(self):
        if self.energia <= 0 or self.hambre <=1:
            print ('el gato no quiero jugar')
        else:
            self.energia -=1
            self.hambre -= 2
            print ('al gato le encanta jugar')
 
    def dormir(self, horas):
        self.energia += horas
        print ('el gato tomo una siesta')

gato = Gato(3,3)
gato.rugido() 
gato.sientate()
Es importante también repasar el concepto de polimorfimo, es decir el poder realizar algo de muchas maneras diferentes o bien, poder responder a la misma función de varias maneras, en el ejemplo siguiente utilizamos una sola función para acceder a dos funciones distintas en diferentes clases.
class Gato:
   def  ruge(self):
       print ('El gato maulla')
 
class Perro:
   def ruge(self):
       print ('El perro ladra')
 
def rugir(animal):
    animal.ruge()

gato = Gato()
perro = Perro()
 
rugir(gato)
rugir(perro)

El último concepto que vamos a tratar en este post es sobre el encapsulamiento de los métodos y variables, esto se refiere a impedir la visualización o acceso de las variables de manera directa. Para declarar una variable o función como privada, el nombre de la función o variable a ser declarado debe comenzar con doble guion abajo. Esto bastará para que lo declarado sea reconocido como privado.

El siguiente ejemplo muestra cómo utilizar el encapsulamiento en métodos y variables.
class Encapsulamiento:
   def __init__(self):
       self.publico = 'variable publica'
       self.__privado = 'variable privada'
 
   def obtener_privado(self):
       print (self.__privado)

Ejemplo = Encapsulamiento()
print (Ejemplo.publico)
#print (Ejemplo.__privado)  Error
Ejemplo.obtener_privado()
Todos los ejemplos fueron tomados del siguiente link. Puede descargarlos desde el siguiente link