Martin Kirchgessner
22 mai 2025
Martin Kirchgessner
Pythoniste depuis la v2.4, à temps plein depuis 8 ans.
J'ai bossé avec Python asynchrone sur :
(né en 2008)
import asyncore
import asynchat
class Bot(asynchat.async_chat):
...
def handle_connect(self):
...
def collect_incoming_data(self, data):
...
def found_terminator(self):
...
...
bot = Bot("server.irc.net", 6667)
asyncore.loop()
$ python3.12 irc.py
Traceback (most recent call last):
File "/home/martin/irc.py", line 15, in <module>
import asyncore
ModuleNotFoundError: No module named 'asyncore'
Qu'est-ce qui pourrait mal se passer ?
async/await
presque partoutrequests → httpx
async def monde_nouveau()
await moralité()
The C10K problem , en 1999
You can buy a 1000MHz machine with 2 GB of RAM and a GB Ethernet card [...]
at 20000 clients, that's 50KHz, 100Kbytes, and 50Kbits/sec per client.
ça devrait suffire pour une réponse de 1-5Ko
et donc: NGinx, NodeJS
while True:
e = eventQueue.dequeue()
processEvent(e)
eventQueue
remplie "par l'extérieur"
Python is super()
"il peut tout faire"
Même des boucles d'évènements.
Quand | Version | Quoi |
---|---|---|
Avant | 2.x | gevent, Tornado, Twisted, asyncore |
2012 | 3.3 | PEP3156 module asyncio |
2015 | 3.5 | PEP492 async/await |
2018 | ASGI |
import asyncio
async def on_connect(reader, writer):
line = await reader.readline()
writer.write(line)
await writer.drain()
async def main(host, port):
srv = await asyncio.start_server(on_connect, host, port)
await srv.serve_forever()
asyncio.run(main('127.0.0.1', 8888))
(peut inclure votre app WSGI)
Concurrence, synchro, verrous, ...
async def monde_nouveau()
__main__.py
asyncio.run(main)
plutôt que main()
Ou bien...
loop.run_until_complete(main)
,
loop.call_soon(main)
,
loop.run_forever(main)
... utilisez un framework !
async def ma_fonction():
x = 1
y = await service.envoi(x)
return y
y = await ma_fonction()
coroutine = ma_fonction() # ⚠️
Coroutine[YieldType, SendType, ReturnType]
async def ma_fonction() -> str:
return "restons simples"
y = await ma_fonction()
if await fut() + 1:
...
pair = await fut(), 'spam'
await foo()['spam'].baz()()
if (await fut()) + 1:
...
pair = (await fut()), 'spam'
await ( foo()['spam'].baz()() )
def order(request):
order = db.save_order(request)
order.init_packaging()
order.email_confirmation()
return templates.confirm(order)
async def order(request):
order = await db.save_order(request)
app.add_background_task(order.init_packaging())
app.add_background_task(order.email_confirmation())
return templates.confirm(order)
from quart import websocket
@app.websocket('/echo')
async def echo_ws():
while True:
data = await websocket.receive()
await websocket.send(data)
async def cancel_me():
print('cancel_me(): before sleep')
try:
# Wait for 1 hour
await asyncio.sleep(3600)
except asyncio.CancelledError:
print('cancel_me(): cancel sleep')
raise
finally:
print('cancel_me(): after sleep')
async def iter_stream():
async for item in OnlineCollection():
item.truc()
class OnlineCollection:
async def __anext__(self):
if data := await self.fetch_data():
return data
else:
raise StopAsyncIteration()
async def wait_for_context():
async with OnlineContext() as contexte:
contexte.truc()
class OnlineContext:
async def __aenter__(self):
await distant_logger.ping()
async def __aexit__(self, exc_type, exc, tb):
if exc:
await distant_logger.oops(exc)
else:
await distant_logger.pong()
Evitez de yield
sous un with
PEP 789
pourrait introduire with sys.prevent_yields(reason)
dans Python 3.14
PYTHONASYNCIODEBUG=1 python mon_appli.py
# OU
asyncio.run(..., debug=True)
# OU
loop.set_debug()
Essentiel pour voir:
await
pip install aiomonitor
async def wrapped_main():
loop = asyncio.get_running_loop()
with aiomonitor.start_monitor(loop):
await real_main()
loop.run_in_executor(fonction_synchrone)
Il reste le GIL, aussi.
⚠️ asyncio n'est pas thread-safe ⚠️
⚠️ attendre le réseau, pas le disque ⚠️
Par exemple aiosqlite fait tout dans un thread.
FastAPI/Quart
+ uvicorn/hypercorn
pytest_asyncio
c'est rock'n'roll
await moralité()
Petite appli Quart/SQLite
~80 req/s dans 200Mb de RAM
+ un upload de 2GB de même temps:Télécharger 12000 petits objets depuis S3
session = requests.Session()
adapter = requests.adapters.HTTPAdapter(
pool_connections=200, pool_maxsize=200
)
session.mount("https://", adapter)
executor = concurrent.futures.ThreadPoolExecutor(max_workers=200)
for _ in executor.map(session.get, urls_generator()):
pass
720 req/s, 240MB RAM
Télécharger 12000 petits objets depuis S3
tasks = []
for url in urls_generator():
tasks.append(asyncio.create_task(aiohttp.get(url)))
asyncio.gather(tasks)
620 req/s, 380MB RAM 📉
Les deux implem ne sont pas à 100% de CPU 🤔
Mais, résistant à la charge
Vous avez déjà une appli ... 🤷
Vous vous souvenez du bot du début ?
L'équivalent "moderne" de asynchat
sont les
Transport/Protocol
Une appli WSGI ça peut suffire aussi.
Merci d'avoir suivi ! Les slides sont sur https://mkir.ch