Asynchroniczny menedżer kontekstu to obiekt, który definiuje asynchroniczne metody __aenter__ i __aexit__ i jest używany w konstrukcji async with. Taki menedżer jest potrzebny do poprawnego otwierania/zamykania zasobów w funkcjach asynchronicznych: połączenia z bazą danych, pliki, sesje itp.
Przykład implementacji:
class AsyncDBConnection: async def __aenter__(self): self.conn = await async_db_connect() return self.conn async def __aexit__(self, exc_type, exc, tb): await self.conn.close() async def main(): async with AsyncDBConnection() as conn: await conn.query('SELECT 1')
Asynchroniczny kontekst pozwala na nieblokowanie pętli zdarzeń technicznymi opóźnieniami, zwiększając wydajność programów współbieżnych.
Czy można używać zwykłego with wewnątrz async def?
Odpowiedź: Tak, ale jeśli operacje wewnątrz menedżera kontekstu używają awaitable (wymagają await), potrzebny jest właśnie async with. Zwykły with nie obsługuje asynchronicznego enter/exit, zablokuje pętlę zdarzeń lub spadnie, jeśli zostanie wywołany await wewnątrz enter/exit.
Przykład:
async def foo(): with open('file.txt') as f: # to ok, odczyt jest synchronny data = f.read() # Ale nie można użyć await wewnątrz zwykłego kontekstu, tylko wewnątrz async with
Historia
Projekt: Serwis internetowy z asynchronicznym API.
Problem: Menedżer połączenia z bazą używał zwykłego with, ale wewnątrz był wywoływany await. Doprowadziło to do błędu RuntimeError("Cannot use 'await' outside async function") i zablokowań pętli zdarzeń w produkcji.
Historia
Projekt: Czat na Websocketach.
Problem: Przy pracy z połączeniami nie były zamykane zasoby websocketowe (używano zwykłego synchronnego menedżera), co prowadziło do wycieków pamięci i zablokowanych połączeń.
Historia
Projekt: Wiele wątkowa asynchroniczna kolejka zadań.
Problem: Menedżer zadań niepoprawnie zaimplementował
__aexit__, zapomniano zwrócić awaitable. Z tego powodu zakończenie zadań nie zachodziło gwarantowanie, a część zadań "zawieszała się" w systemie.