Programación Concurrente

Lenguajes de Programación
Ingeniería en Computación e Informática
Semestre 2019-1

Thread con Python

Concurrencia en Python

Multithreading & Multiprocessing

Multithreading 

Características

  • La ejecución de los threads en Python está controlada por el GIL (Global Interpreter Lock)
  • Sólo un thread puede ejecutarse a la vez, independientemente del número de procesadores con el que cuente la máquina
  • Entre los hilos se comparte las secciones de datos y código.
  • Cuentan con identificador que es único.
  • Disponen de su controlador de programa.
  • Tienen su propio conjunto de registros de CPU y pila.

 

Ventajas

  • Los hilos en ejecución de un proceso comparten el mismo espacio de datos que el hilo principal
    • Tienen acceso a la misma información
    • Se pueden comunicarse entre sí más fácilmente que si estuvieran en procesos separados.
  • Ejecutar un proceso de varios hilos suele requerir menos recursos de memoria que ejecutar lo equivalente en procesos separados.
  • Permite simplificar el diseño de las aplicaciones que necesitan ejecutar varias operaciones concurrentemente.
  • Simplicidad de operación gracias al uso de GIL

 

Desventajas

  • Limitaciones del Global Interpreter Lock

  • El GIL de CPython evita que múltiples threads ejecuten bytecode simultáneamente

  • El resultado puede variar según el orden en que lleguen los hilos (condición de carrera).

  • Se debe  controlar el acceso a los datos, utilizando lock, condiciones o semáforos.

  • Paralelismo

El objeto Thread

import threading
def worker():
    """funcion que realiza el trabajo en el thread"""
    print 'Estoy trabajando LP'
    return
threads = []
for i in range(3):
    t = threading.Thread(target=worker)
    threads.append(t)
    t.start()

El modo más sencillo para usar un hilo es instanciar un objeto de la clase Thread con una función objetivo y hacer una llamada a su método start().

 

El objeto Thread con argumentos

import threading
def worker(count):
    """funcion que realiza el trabajo en el thread"""
    print "Este es el %s trabajo que hago hoy para LDP" % count
    return
threads = list()
for i in range(3):
    t = threading.Thread(target=worker, args=(i,))
    threads.append(t)
    t.start()
thread_1.py

A los threads se les puede pasar parámetros que después son usados por la función objetivo. Cualquier tipo de objeto puede ser pasado como parámetro a un thread.

El objeto Thread con argumentos (ii)

import threading

def contar(num_hilo, **datos):
    contador = datos['inicio']
    incremento = datos['incremento']
    limite = datos['limite']
    while contador<=limite:                
        # print('hilo:', num_hilo, 'contador:', contador)  
        print 'hilo: %d | contador: %d \n' % (num_hilo,contador)      
        contador+=incremento

for num_hilo in range(3):
    hilo = threading.Thread(target=contar, 
                            args=(num_hilo,),
                            kwargs={'inicio':0, 
                                    'incremento':1,
                                    'limite':3})
    hilo.start()
thread_2.py

¿Como saber en que Thread nos encontramo?

thread_3.py
  • Se pueden usar argumentos para nombrar los threads que creamos aunque no es necesario.
  • Cada instancia de la clase Thread tiene un nombre asignado por defecto.
  • Nombrar los threads puede ser útil por ejemplo, a la hora de clarificar nuestro código.

 

¿Como saber en que Thread nos encontramo?

import threading
import time

def worker():
    print threading.currentThread().getName(), 'Lanzado'
    time.sleep(2)
    print threading.currentThread().getName(), 'Deteniendo'

def servicio():
    print threading.currentThread().getName(), 'Lanzado'
    print threading.currentThread().getName(), 'Deteniendo'

if __name__ == '__main__':
        
    t = threading.Thread(target=servicio, name='Servicio')
    w = threading.Thread(target=worker, name='Worker')
    z = threading.Thread(target=worker)
    w.start()
    z.start()
    t.start()
thread_3.py

Usando el módulo logging

import threading
import logging
import time
logging.basicConfig( level=logging.DEBUG,
    format='[%(levelname)s] - %(threadName)-10s : %(message)s')


def worker():
    logging.debug('Lanzado')
    time.sleep(2)
    logging.debug('Deteniendo')

if __name__ == '__main__':
        
    w = threading.Thread(target=worker, name='Worker')
    w.start()
thread_4.py

El módulo logging soporta la inclusión del nombre del hilo de forma nativa

Daemon Threads

import threading
import logging
import time
logging.basicConfig( level=logging.DEBUG,
    format='[%(levelname)s] - %(threadName)-10s : %(message)s')

def daemon():
    logging.debug('Lanzado')
    time.sleep(2)
    logging.debug('Deteniendo')

d = threading.Thread(target=daemon, name='Daemon')
d.setDaemon(True)
d.start()
thread_5.py
  • Permite lanzar un thread sin bloquear el thread principal, permitiendo salir en cualquier momento
  • Comportamiento util para procesos críticos, donde si el hilo muero no genera corrupción de datos

Daemon Threads (ii)

def chequear(nombre):

    contador = 0
    tam = 0
    while contador<100:
        contador += 1
        if os.path.exists(nombre):
            estado = os.stat(nombre)  
            tam = estado.st_size
        
        logging.debug(
            '[%d] %s | %d | tam: %d bytes',
            contador,
            threading.current_thread().getName(),
            contador,
            tam
        )

        time.sleep(0.1)        

def escribir(nombre):
    '''Escribe en archivo'''    
    contador = 1 
    while contador<=10:
        with open(nombre, 'a') as archivo:
            archivo.write('lorem\n')
            logging.debug(
                '%s | %d ',
                threading.current_thread().getName(),
                contador
            )
            time.sleep(0.3)
            contador += 1

if __name__ == '__main__':
        
    nombre = 'archivo.txt'
    if os.path.exists(nombre):
        os.remove(nombre)

    hilo1 = threading.Thread(name='chequear', 
                             target=chequear,
                             args=(nombre,),
                             daemon=True)
    # hilo1.setDaemon(True)
                             
    hilo2 = threading.Thread(name='escribir',
                             target=escribir,
                             args=(nombre,))
    hilo1.start()
    hilo2.start()
thread_6.py

Daemon Threads (iii)

def chequear(nombre):

    contador = 0
    tam = 0
    while contador<100:
        contador += 1
        if os.path.exists(nombre):
            estado = os.stat(nombre)  
            tam = estado.st_size
        
        logging.debug(
            '[%d] %s | %d | tam: %d bytes',
            contador,
            threading.current_thread().getName(),
            contador,
            tam
        )

        time.sleep(0.1)        

def escribir(nombre):
    '''Escribe en archivo'''    
    contador = 1 
    while contador<=10:
        with open(nombre, 'a') as archivo:
            archivo.write('lorem\n')
            logging.debug(
                '%s | %d ',
                threading.current_thread().getName(),
                contador
            )
            time.sleep(0.3)
            contador += 1

if __name__ == '__main__':
        
    nombre = 'archivo.txt'
    if os.path.exists(nombre):
        os.remove(nombre)

    hilo1 = threading.Thread(name='chequear', 
                             target=chequear,
                             args=(nombre,),
                             daemon=True)
                             
    hilo2 = threading.Thread(name='escribir',
                             target=escribir,
                             args=(nombre,))
    hilo1.start()
    hilo2.start()


    hilo1.join()
    print(hilo1.isAlive())
thread_7.py

Controlar la ejecución de varios demonios

import threading
import logging
logging.basicConfig( level=logging.DEBUG,
    format='[%(levelname)s] - %(threadName)-10s : %(message)s')

def contar(numero):
    contador = 0
    while contador<10:
        contador+=1
        # print(numero, threading.get_ident(), contador)
        logging.debug(
            'numero: %d | id: %s | contador: %d',
            numero, 
            str(threading.get_ident()), 
            contador
        )

for numero in range(1, 11):
    hilo = threading.Thread(target=contar, 
                            args=(numero,), 
                            daemon=True)
    hilo.start()


# Obtiene hilo principal
hilo_ppal = threading.main_thread()

# Recorre hilos activos para controlar estado de su ejecución
for hilo in threading.enumerate():

    # Si el hilo es hilo_ppal continua al siguiente hilo activo
    if hilo is hilo_ppal:
        continue

    # Se obtiene información hilo actual y núm. hilos activos
    logging.debug(
        'name: %s | id: %s | dameon: %d | active_count: %d ',
        hilo.getName(), 
        hilo.ident, 
        hilo.isDaemon(), 
        threading.active_count()
    )

    # El programa esperará a que este hilo finalice:
    hilo.join()
thread_8.py

Threads con temporizador

import threading
import time
import logging
logging.basicConfig( level=logging.DEBUG,
    format='[%(levelname)s] - %(threadName)-10s : %(message)s')

def retraso():
    logging.debug('worker en ejecución')
    return

th1 = threading.Timer(2, retraso)
th1.setName('th1')

th2 = threading.Timer(2, retraso)
th2.setName('th2')

logging.debug('lanzando temporizadores')
th1.start()
th2.start()
logging.debug('esperando antes de cancelar a %s', th2.getName())
time.sleep(1)
logging.debug('cancelando a %s', th2.getName())
th2.cancel()
logging.debug('hecho')
thread_11.py

Sincronización de Threads

Control del acceso a los recursos. Bloqueos (Lock)

  • Los objetos Lock() permiten gestionar los bloqueos que evitan que los hilos modifiquen variables compartidas al mismo tiempo.
  • El método acquire() permite que un hilo bloquee a otros hilos en un punto del programa
  • El método release() libera el bloqueo. En el momento que se produzca el desbloqueo otro hilo (o el mismo) podrá bloquear de nuevo. 

 

Control del acceso a los recursos. Bloqueos (ii)

import threading

total = 0

def acumula5():
    global total
    contador = 0
    hilo_actual = threading.current_thread().getName()
    while contador < 20:
        print('Esperando para bloquear', hilo_actual)
        bloquea.acquire()
        try:
            contador = contador + 1
            total = total + 5
            print('Bloqueado por', hilo_actual, contador)
            print('Total', total)
            
        finally:
            print('Liberado bloqueo por', hilo_actual)
            bloquea.release()
    
bloquea = threading.Lock()    
hilo1 = threading.Thread(name='h1', target=acumula5)
hilo2 = threading.Thread(name='h2', target=acumula5)
hilo1.start()
hilo2.start()
thread_14.py

Control del acceso a los recursos. Bloqueos (iii)

thread_14.py
  • Para conocer si otro hilo ha adquirido el bloqueo sin mantener al resto de subprocesos detenidos hay que asignar al argumento blocking de acquire() el valor False.
  • De esta forma se pueden realizar otros trabajos mientras se espera a tener éxito en un bloqueo y controlar el número de reintentos realizados.
  • El método locked() se puede utilizar para verificar si un bloqueo se mantiene en un momento dado

 

Control del acceso a los recursos. Bloqueos (iii)

import threading

def acumula5():
    global total
    contador = 0
    hilo_actual = threading.current_thread().getName()
    num_intentos = 0
    while contador < 20:
        lo_consegui = bloquea.acquire(blocking=False)
        try:
            if lo_consegui:
                contador = contador + 1
                total = total + 5
                print('Bloqueado por', hilo_actual, contador)
                print('Total', total,hilo_actual)
            else:
                num_intentos+=1
                print('Número de intentos de bloqueo', 
                      num_intentos,
                      'hilo',
                      hilo_actual, 
                      bloquea.locked())
                print(hilo_actual,'Hacer otro trabajo')
            
        finally:
            if lo_consegui:
                print('Liberado bloqueo por', hilo_actual)
                bloquea.release()
    
total = 0
bloquea = threading.Lock()    
hilo1 = threading.Thread(name='h1', target=acumula5)
hilo2 = threading.Thread(name='h2', target=acumula5)
hilo1.start()
hilo2.start()
thread_15.py

Sincronizar hilos con objetos Event

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.

Sincronizar hilos con objetos Event

import threading, random

def gen_pares():
    num_pares = 0
    print('Números:', end=' ')
    while num_pares < 25:
        numero = random.randint(1, 10)
        resto = numero % 2
        if resto == 0:
            num_pares +=1
            print(numero, end=' ')
    print()        

def contar():
    contar = 0
    nom_hilo = threading.current_thread().getName()
    print(nom_hilo, "en espera")
    estado = evento.wait()
    while contar < 25:
        contar+=1
        print(nom_hilo, ':', contar)
    
evento = threading.Event()
hilo1 = threading.Thread(name='h1', target=contar)
hilo2 = threading.Thread(name='h2', target=contar)
hilo1.start()
hilo2.start()

print('Obteniendo 25 números pares...')
gen_pares()
print('Ya se han obtenido')
evento.set()
thread_16.py

Sincronizar hilos con objetos Event (ii)

import threading

def avanza(evento):
    ciclo = 0
    valor = 0
    while valor < 20:
        estado = evento.wait()
        if estado:
            ciclo+=1
            valor+=1
            print('avanza', valor)
            if ciclo == 10 and hilo2.isAlive():
                evento.clear()
                ciclo = 0
    print('avanza: ha finalizado')

def retrocede(evento, tiempo):
    ciclo = 0
    valor = 21
    while valor > 1:
        estado = evento.wait(tiempo)
        if not estado:
            ciclo+=1
            valor-=1
            print('retrocede', valor)
            if ciclo == 5 and hilo1.isAlive():
                evento.set()
                ciclo = 0
    print('retrocede: ha finalizado')
            
evento = threading.Event()
hilo1 = threading.Thread(target=avanza, 
                         args=(evento,),)
hilo2 = threading.Thread(target=retrocede, 
                         args=(evento, 0.5),)
hilo1.start()
hilo2.start()
thread_17.py

Recursos

  • http://python-para-impacientes.blogspot.cl/2016/12/threading-programacion-con-hilos-i.html
  • https://www.ciberbyte.com/programacion/python/multi-hilo-python/
  • https://www.genbetadev.com/python/multiprocesamiento-en-python-threads-a-fondo-introduccion

Thread con python

By Miguel Cantillana

Thread con python

Laboratorio

  • 675