Presentación de la charla impartida en el meetup de Python Madrid sobre Asincronía en Python https://www.meetup.com/es-ES/python-madrid/events/268111847/
14. BAR
Una Hamburguesa,
con patatas grandes
y una cerveza,
por favor Enseguida!
esperar esperar servir la cerveza
pedir la hamburguesa
a la cocina
poner las patatatas
a freir
sacar las patatas
de la freidora
1 cliente = 10 min
15. BAR
esperar
poner las
patatatas a freir
pedir la
hamburguesa a la
cocina
esperar servir la cerveza
sacar las patatas
de la freidora
esperar
poner las
patatatas a freir
pedir la
hamburguesa a la
cocina
esperar servir la cerveza
sacar las patatas
de la freidora
esperar
poner las
patatatas a freir
pedir la
hamburguesa a la
cocina
esperar servir la cerveza
sacar las patatas
de la freidora
3 clientes (10, 20, 30) ⋍ 20 min
16. atender más
clientes a la vez
esperar
poner las
patatatas a freir
pedir la
hamburguesa a la
cocina
esperar servir la cerveza
sacar las patatas
de la freidora
esperar
poner las
patatatas a freir
pedir la
hamburguesa a la
cocina
esperar servir la cerveza
sacar las patatas
de la freidora
esperar
poner las
patatatas a freir
pedir la
hamburguesa a la
cocina
esperar servir la cerveza
sacar las patatas
de la freidora
atender a un cliente
en menos tiempo
hacer un mejor
uso de los recursos
19. Tiempos
System Event Actual Latency Scaled Latency
One CPU cycle 0.4 ns 1 s
Level 1 cache access 0.9 ns 2 s
Level 2 cache access 2.8 ns 7 s
Level 3 cache access 28 ns 1 min
Main memory access (DDR DIMM) ~100 ns 4 min
NVMe SSD I/O ~25 μs 17 hrs
SSD I/O 50–150 μs 1.5 - 4 days
Rotational disk I/O 1–10 ms 1-9 months
Internet call: San Francisco to New York
City
65 ms 5 years
Internet call: San Francisco to Hong Kong 141 ms 11 years
20. url = 'http://example.com/david-hasselhoff.jpg'
path = 'media/cool-pics/screen-background.jpg’
r = requests.get(url, stream=True)
if r.status_code == 200:
with open(path, 'wb') as f:
for chunk in r.iter_content(1024):
f.write(chunk)
im = Image.open(path)
im.thumbnail(size, Image.ANTIALIAS)
im.save(thumbnail_path, "JPEG")
pic = CoolPics(img=path, thumbnail=thumbnail_path)
pic.save()
varios años
varios meses
varios meses
varios meses
varios meses
unas pocas horas
varios meses
< de una hora
varios meses
21. url = 'http://example.com/david-hasselhoff.jpg'
path = 'media/cool-pics/screen-background.jpg’
r = requests.get(url, stream=True)
if r.status_code == 200:
with open(path, 'wb') as f:
for chunk in r.iter_content(1024):
f.write(chunk)
im = Image.open(path)
im.thumbnail(size, Image.ANTIALIAS)
im.save(thumbnail_path, "JPEG")
pic = CoolPics(img=path, thumbnail=thumbnail_path)
pic.save()
varios años
varios meses
varios meses
varios meses
varios meses
unas pocas horas
varios meses
< de una hora
varios meses
naturalezasecuencial
x 2000
23. BAR
esperar
poner las
patatatas a freir
pedir la
hamburguesa a la
cocina
esperar servir la cerveza
sacar las patatas
de la freidora
esperar
poner las
patatatas a freir
pedir la
hamburguesa a la
cocina
esperar servir la cerveza
sacar las patatas
de la freidora
esperar
poner las
patatatas a freir
pedir la
hamburguesa a la
cocina
esperar servir la cerveza
sacar las patatas
de la freidora
3 clientes (10, 12, 20) ⋍ 14 min
24. import threading
import time
class MyThread(threading.Thread):
def __init__(self, thread_id, name, delay):
super().__init__()
self.thread_id = thread_id
self.name = name
self.delay = delay
def run(self):
print("Starting " + self.name)
# Get lock to synchronize threads
# g_thread_lock.acquire()
print_time(self.name, 3, self.delay)
# Free lock to release next thread
# g_thread_lock.release()
def print_time(thread_name, counter, delay):
while counter:
time.sleep(delay)
print("%s: %s" % (
thread_name,
time.ctime(time.time()))
)
counter -= 1
# …
# …
g_thread_lock = threading.Lock()
threads = []
# Create new threads
thread1 = MyThread(1, "Thread-1", 1)
thread2 = MyThread(2, "Thread-2", 2)
# Start new Threads
thread1.start()
thread2.start()
# Add threads to thread list
threads.append(thread1)
threads.append(thread2)
# Wait for all threads to complete
for t in threads:
t.join()
print("Exiting Main Thread")
threading
25. import threading
import time
class MyThread(threading.Thread):
def __init__(self, thread_id, name, delay):
super().__init__()
self.thread_id = thread_id
self.name = name
self.delay = delay
def run(self):
print("Starting " + self.name)
# Get lock to synchronize threads
# g_thread_lock.acquire()
print_time(self.name, 3, self.delay)
# Free lock to release next thread
# g_thread_lock.release()
def print_time(thread_name, counter, delay):
while counter:
time.sleep(delay)
print("%s: %s" % (
thread_name,
time.ctime(time.time()))
)
counter -= 1
# …
# …
g_thread_lock = threading.Lock()
threads = []
# Create new threads
thread1 = MyThread(1, "Thread-1", 1)
thread2 = MyThread(2, "Thread-2", 2)
# Start new Threads
thread1.start()
thread2.start()
# Add threads to thread list
threads.append(thread1)
threads.append(thread2)
# Wait for all threads to complete
for t in threads:
t.join()
print("Exiting Main Thread")
crear las threads
26. import threading
import time
class MyThread(threading.Thread):
def __init__(self, thread_id, name, delay):
super().__init__()
self.thread_id = thread_id
self.name = name
self.delay = delay
def run(self):
print("Starting " + self.name)
# Get lock to synchronize threads
# g_thread_lock.acquire()
print_time(self.name, 3, self.delay)
# Free lock to release next thread
# g_thread_lock.release()
def print_time(thread_name, counter, delay):
while counter:
time.sleep(delay)
print("%s: %s" % (
thread_name,
time.ctime(time.time()))
)
counter -= 1
# …
# …
g_thread_lock = threading.Lock()
threads = []
# Create new threads
thread1 = MyThread(1, "Thread-1", 1)
thread2 = MyThread(2, "Thread-2", 2)
# Start new Threads
thread1.start()
thread2.start()
# Add threads to thread list
threads.append(thread1)
threads.append(thread2)
# Wait for all threads to complete
for t in threads:
t.join()
print("Exiting Main Thread")
arrancarlas
27. import threading
import time
class MyThread(threading.Thread):
def __init__(self, thread_id, name, delay):
super().__init__()
self.thread_id = thread_id
self.name = name
self.delay = delay
def run(self):
print("Starting " + self.name)
# Get lock to synchronize threads
# g_thread_lock.acquire()
print_time(self.name, 3, self.delay)
# Free lock to release next thread
# g_thread_lock.release()
def print_time(thread_name, counter, delay):
while counter:
time.sleep(delay)
print("%s: %s" % (
thread_name,
time.ctime(time.time()))
)
counter -= 1
# …
# …
g_thread_lock = threading.Lock()
threads = []
# Create new threads
thread1 = MyThread(1, "Thread-1", 1)
thread2 = MyThread(2, "Thread-2", 2)
# Start new Threads
thread1.start()
thread2.start()
# Add threads to thread list
threads.append(thread1)
threads.append(thread2)
# Wait for all threads to complete
for t in threads:
t.join()
print("Exiting Main Thread")
¿sincronizarlas?
28. import threading
import time
class MyThread(threading.Thread):
def __init__(self, thread_id, name, delay):
super().__init__()
self.thread_id = thread_id
self.name = name
self.delay = delay
def run(self):
print("Starting " + self.name)
# Get lock to synchronize threads
# g_thread_lock.acquire()
print_time(self.name, 3, self.delay)
# Free lock to release next thread
# g_thread_lock.release()
def print_time(thread_name, counter, delay):
while counter:
time.sleep(delay)
print("%s: %s" % (
thread_name,
time.ctime(time.time()))
)
counter -= 1
# …
# …
g_thread_lock = threading.Lock()
threads = []
# Create new threads
thread1 = MyThread(1, "Thread-1", 1)
thread2 = MyThread(2, "Thread-2", 2)
# Start new Threads
thread1.start()
thread2.start()
# Add threads to thread list
threads.append(thread1)
threads.append(thread2)
# Wait for all threads to complete
for t in threads:
t.join()
print("Exiting Main Thread")
esperar a que terminen
34. BAR BAR
esperar
poner las
patatatas a freir
pedir la
hamburguesa a la
cocina
esperar servir la cerveza
sacar las patatas
de la freidora
esperar
poner las
patatatas a freir
pedir la
hamburguesa a la
cocina
esperar servir la cerveza
sacar las patatas
de la freidora
esperar
poner las
patatatas a freir
pedir la
hamburguesa a la
cocina
esperar servir la cerveza
sacar las patatas
de la freidora
3 clientes (10, 10, 20) ⋍ 13.3 min
35. from multiprocessing import Lock, Process,
Queue, current_process
import time
import queue # imported for using queue.Empty exception
def do_job(tasks_to_accomplish, tasks_that_are_done):
while True:
try:
task = tasks_to_accomplish.get_nowait()
except queue.Empty:
break
else:
print(task)
tasks_that_are_done.put(task +
' is done by ' + current_process().name)
time.sleep(.5)
return True
def main():
number_of_task = 10
number_of_processes = 4
tasks_to_accomplish = Queue()
tasks_that_are_done = Queue()
processes = []
for i in range(number_of_task):
tasks_to_accomplish.put("Task no " + str(i))
# creating processes
for w in range(number_of_processes):
p = Process(
target=do_job,
args=(tasks_to_accomplish, tasks_that_are_done)
)
processes.append(p)
p.start()
# completing process
for p in processes:
p.join()
# print the output
while not tasks_that_are_done.empty():
print(tasks_that_are_done.get())
return True
if __name__ == '__main__':
main()
mulf-processing
36. from multiprocessing import Lock, Process,
Queue, current_process
import time
import queue # imported for using queue.Empty exception
def do_job(tasks_to_accomplish, tasks_that_are_done):
while True:
try:
task = tasks_to_accomplish.get_nowait()
except queue.Empty:
break
else:
print(task)
tasks_that_are_done.put(task +
' is done by ' + current_process().name)
time.sleep(.5)
return True
def main():
number_of_task = 10
number_of_processes = 4
tasks_to_accomplish = Queue()
tasks_that_are_done = Queue()
processes = []
for i in range(number_of_task):
tasks_to_accomplish.put("Task no " + str(i))
# creating processes
for w in range(number_of_processes):
p = Process(
target=do_job,
args=(tasks_to_accomplish, tasks_that_are_done)
)
processes.append(p)
p.start()
# completing process
for p in processes:
p.join()
# print the output
while not tasks_that_are_done.empty():
print(tasks_that_are_done.get())
return True
if __name__ == '__main__':
main()
multi-processing
37. from multiprocessing import Lock, Process,
Queue, current_process
import time
import queue # imported for using queue.Empty exception
def do_job(tasks_to_accomplish, tasks_that_are_done):
while True:
try:
task = tasks_to_accomplish.get_nowait()
except queue.Empty:
break
else:
print(task)
tasks_that_are_done.put(task +
' is done by ' + current_process().name)
time.sleep(.5)
return True
def main():
number_of_task = 10
number_of_processes = 4
tasks_to_accomplish = Queue()
tasks_that_are_done = Queue()
processes = []
for i in range(number_of_task):
tasks_to_accomplish.put("Task no " + str(i))
# creating processes
for w in range(number_of_processes):
p = Process(
target=do_job,
args=(tasks_to_accomplish, tasks_that_are_done)
)
processes.append(p)
p.start()
# completing process
for p in processes:
p.join()
# print the output
while not tasks_that_are_done.empty():
print(tasks_that_are_done.get())
return True
if __name__ == '__main__':
main()
multi-processing
crear colas IPC
38. from multiprocessing import Lock, Process,
Queue, current_process
import time
import queue # imported for using queue.Empty exception
def do_job(tasks_to_accomplish, tasks_that_are_done):
while True:
try:
task = tasks_to_accomplish.get_nowait()
except queue.Empty:
break
else:
print(task)
tasks_that_are_done.put(task +
' is done by ' + current_process().name)
time.sleep(.5)
return True
def main():
number_of_task = 10
number_of_processes = 4
tasks_to_accomplish = Queue()
tasks_that_are_done = Queue()
processes = []
for i in range(number_of_task):
tasks_to_accomplish.put("Task no " + str(i))
# creating processes
for w in range(number_of_processes):
p = Process(
target=do_job,
args=(tasks_to_accomplish, tasks_that_are_done)
)
processes.append(p)
p.start()
# completing process
for p in processes:
p.join()
# print the output
while not tasks_that_are_done.empty():
print(tasks_that_are_done.get())
return True
if __name__ == '__main__':
main()
multi-processing
crear los procesos
39. from multiprocessing import Lock, Process,
Queue, current_process
import time
import queue # imported for using queue.Empty exception
def do_job(tasks_to_accomplish, tasks_that_are_done):
while True:
try:
task = tasks_to_accomplish.get_nowait()
except queue.Empty:
break
else:
print(task)
tasks_that_are_done.put(task +
' is done by ' + current_process().name)
time.sleep(.5)
return True
def main():
number_of_task = 10
number_of_processes = 4
tasks_to_accomplish = Queue()
tasks_that_are_done = Queue()
processes = []
for i in range(number_of_task):
tasks_to_accomplish.put("Task no " + str(i))
# creating processes
for w in range(number_of_processes):
p = Process(
target=do_job,
args=(tasks_to_accomplish, tasks_that_are_done)
)
processes.append(p)
p.start()
# completing process
for p in processes:
p.join()
# print the output
while not tasks_that_are_done.empty():
print(tasks_that_are_done.get())
return True
if __name__ == '__main__':
main()
multi-processing
ejecutar los procesos
40. from multiprocessing import Lock, Process,
Queue, current_process
import time
import queue # imported for using queue.Empty exception
def do_job(tasks_to_accomplish, tasks_that_are_done):
while True:
try:
task = tasks_to_accomplish.get_nowait()
except queue.Empty:
break
else:
print(task)
tasks_that_are_done.put(task +
' is done by ' + current_process().name)
time.sleep(.5)
return True
def main():
number_of_task = 10
number_of_processes = 4
tasks_to_accomplish = Queue()
tasks_that_are_done = Queue()
processes = []
for i in range(number_of_task):
tasks_to_accomplish.put("Task no " + str(i))
# creating processes
for w in range(number_of_processes):
p = Process(
target=do_job,
args=(tasks_to_accomplish, tasks_that_are_done)
)
processes.append(p)
p.start()
# completing process
for p in processes:
p.join()
# print the output
while not tasks_that_are_done.empty():
print(tasks_that_are_done.get())
return True
if __name__ == '__main__':
main()
multi-processing
esperar a que
terminen todos
41. I/O bound CPU bound
multi-
threading
mulf-
processing
comparten
memoria
GIL
más pesados
no comparten
memoria
IPC
42. Global Interpreter Lock
• El intérprete de Python no es thread-safe
• Específico de ALGUNAS implementaciones de Python (CPython, PyPy)
• Solo una thread puede estar interpretando código Python en un
proceso
• El intérprete “suelta” el “lock” para operaciones I/O (o incluso NumPy)
• Consecuencias:
• tareas limitadas por I/O van más rápido con múltiples threads
• tareas limitadas por CPU van más lento con múltiples threads
43. más rápido
¡más lento!
(por culpa del GIL)
más rápido
(pero más ”caro”)
más rápido
I/O bound CPU bound
multi-
threading
multi-
processing
comparten
memoria
GIL
más pesados
no comparten
memoria
IPC
45. BAR
servir la cerveza
pedir la hamburguesa a la
cocina
poner las patatatas a
freir
sacar las patatas de
la freidora
servir la cerveza
pedir la hamburguesa a la
cocina
poner las patatatas a
freir
sacar las patatas de
la freidora
servir la cerveza
pedir la hamburguesa a la
cocina
poner las patatatas a
freir
sacar las patatas de
la freidora
3 clientes (4,5,7) ⋍ 5,33 min
46. Modelo de Concurrencia de JavaScript
• en JavaScript solo hay una thread de usuario
• el código se ejecuta “por turnos”, sin ninguna interrupción, siempre hasta
el final
• dentro de un “bucle de eventos”
• TODAS las operaciones de I/O son asíncronas:
• se lanzan en el momento
• el código no espera a que se termine la ejecución
• el resultado se recibe en un callback (o una Promesa) que se ejecutará en un turno
en el futuro
• También son asíncronas todas las funciones que llaman a una función
asíncrona
https://developer.mozilla.org/en-US/docs/Web/JavaScript/EventLoop
56. si no hay cooperación, no funciona
• en JavaScript la cooperación es
obligatoria
• todas las librerías son asíncronas
• en Python casi todas las librerías
son síncronas (no cooperan)
nuestro código no debe bloquearse
debe devolver el control al bucle de eventos
no hay un “scheduler pre-emptivo”
57. el problema es…
• todo lo que NO podemos usar
• http, requests
• acceso a bases de datos
• I/O de ficheros
• necesitamos nuevas librerías para todo
• aiohttp
• aiopg
• aiomysql
• aiofiles
• https://github.com/timofurrer/awesome-asyncio
59. es difícil
• converfr un proyecto existente en async
• o una parte
• en un nuevo proyecto async
• encontrar y usar librerías
• p.ej script que se conecta a nuestra cuenta de GitHub y crea un informe de
branches en Excel
• pygithub - hace llamadas de red
• openpyxl - lee y escribe ficheros
63. concurrent.futures
• librería de alto nivel
• usando multi-processing o multi-threading con la misma interfaz
• proporciona una sintaxis parecida a las promesas
• compatible con todas las librerías síncronas
• si son thread-safe podemos usar multi-threading
• si no, tendremos que usar multi-processing
Python 3.22011-02-20
64. import threading
from concurrent import futures
from random import random
from time import sleep
import requests
nums = range(1, 11)
url_tpl = "http://jsonplaceholder.typicode.com/todos/{}"
def get_data(myid):
url = url_tpl.format(myid)
data = requests.get(url).json()
sleep(random() * 5)
return data
def main():
with futures.ThreadPoolExecutor(4) as executor:
results = executor.map(get_data, nums)
print()
for result in results:
print(result)
if __name__ == '__main__':
main()
futures
65. import threading
from concurrent import futures
from random import random
from time import sleep
import requests
nums = range(1, 11)
url_tpl = "http://jsonplaceholder.typicode.com/todos/{}"
def get_data(myid):
url = url_tpl.format(myid)
data = requests.get(url).json()
sleep(random() * 5)
return data
def main():
with futures.ThreadPoolExecutor(4) as executor:
results = executor.map(get_data, nums)
print()
for result in results:
print(result)
if __name__ == '__main__':
main()
futures
ejecutar en 4 threads
66. import threading
from concurrent import futures
from random import random
from time import sleep
import requests
nums = range(1, 11)
url_tpl = "http://jsonplaceholder.typicode.com/todos/{}"
def get_data(myid):
url = url_tpl.format(myid)
data = requests.get(url).json()
sleep(random() * 5)
return data
def main():
with futures.ThreadPoolExecutor(4) as executor:
results = executor.map(get_data, nums)
print()
for result in results:
print(result)
if __name__ == '__main__':
main()
futures
¡librerías síncronas!
67. def main():
# with futures.ProcessPoolExecutor(4) as executor:
with futures.ThreadPoolExecutor(4) as executor:
jobs = [executor.submit(get_data, num) for num in nums]
for comp_job in futures.as_completed(jobs):
print(comp_job.result())
futures
resultados según están
disponibles
68. def main():
with futures.ThreadPoolExecutor(5) as executor:
jobs = [executor.submit(get_img, num) for num in range(20)]
for comp_job in futures.as_completed(jobs):
img_path = comp_job.result()
executor.submit(add_meme_to_img, img_path)
futures
pasos encadenados
69. def main():
# with futures.ProcessPoolExecutor(4) as executor:
with futures.ThreadPoolExecutor(4) as executor:
jobs = [executor.submit(get_data, num) for num in nums]
done, not_done = futures.wait(jobs, return_when=futures.FIRST_COMPLETED)
print(done.pop().result())
print(len(not_done))
for not_done_job in not_done:
not_done_job.cancel()
print()
print('finished')
futures
“carreras” de futuros
70. ¿y lo de la thread-safety?
• evitar los efectos secundarios o variables compartidas
• funciones puras - programación funcional
• cuidado con las librerías que usamos, pueden no ser thread-safe
73. vistas con múlfples
llamadas I/O
(disco/red/bd)
vistas con múlfples
llamadas I/O
(disco/red/bd)
Asincronía en Django
tareas de larga
duración
fuera del ciclo de
request/response
websockets con
muchos clientes
¡ESTO!
async/await
celery
80. resumiendo
• el mundo es asíncrono
• muchas aplicaciones son I/O bound
• podemos usar mejor los recursos:
• multi-proceso,
• multi-thread
• concurrencia cooperativa
• cada tarea se puede completar en menos tiempo
• paralelizar acciones necesarias para completar una tarea (a veces)
• con los mismos recursos puedo ejecutar más tareas
• paralelizar acciones necesarias para múltiples tareas