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