Python'da Asenkron Fonksiyonlar Yazmak
Selamlar, bu yazımda Python'da async/await ile coroutine yazmaya bakacağız. Konuyu hiç görmemiş arkadaşlar için temelden başlayacağım, ama asıl niyetim 'merhaba dünya' demek değil; asyncio'nun nerede işe yaradığını, nerede sizi yanıltacağını ve threading ile neden aynı şey olmadığını netleştirmek. Hadi başlayalım.
Önce event loop, sonra coroutine
Asenkron Python'un kalbinde event loop var. Tek thread, tek loop, ama bu loop iş bittikçe sıraya soktuğu coroutine'leri (eş yordam) çalıştırıyor. Yani aynı anda binlerce iş 'çalışıyormuş gibi' görünür ama aslında her an tek bir tanesi CPU'da. Geri kalanı bir şey beklerken (await noktasında) loop kontrolü alıp başkasını sürüyor.
Bunu kafanızda oturtmak için en kısa yol kod:
import asyncio
async def selam(ad: str) -> str:
print(f'{ad} basliyor')
await asyncio.sleep(1)
print(f'{ad} bitiyor')
return f'Selam {ad}'
async def main():
sonuc = await selam('Ayse')
print(sonuc)
asyncio.run(main())
async def ile tanımlanan fonksiyon çağrıldığında çalışmaz, sadece bir coroutine nesnesi döndürür. Çalıştırmak için ya await etmeniz, ya da event loop'a vermeniz gerek. asyncio.run da tam bunu yapıyor: yeni bir loop kuruyor, main'i ona veriyor, bittiğinde loop'u kapatıyor.
Eşzamanlı çalıştırmak: gather ve create_task
İşlerin paralel akmasını istediğimizde await zincirine girmek tek başına yetmez. Şu kod aslında seri çalışır, çünkü her satırda durup bekliyoruz:
sonuc1 = await veri_cek('A', 1)
sonuc2 = await veri_cek('B', 1)
sonuc3 = await veri_cek('C', 1)
Üç tane bir saniyelik istek varsa toplam üç saniye. Hâlbuki asyncio'nun derdi tam buradaydı. Çözüm asyncio.gather:
import asyncio
async def veri_cek(kaynak: str, gecikme: float) -> dict:
await asyncio.sleep(gecikme)
return {'kaynak': kaynak, 'veri': f'{kaynak} verisi'}
async def main():
sonuclar = await asyncio.gather(
veri_cek('A', 1),
veri_cek('B', 1),
veri_cek('C', 1),
)
print(sonuclar)
asyncio.run(main())
Bu sefer toplam süre yaklaşık bir saniye. Üçü de aynı anda beklemeye geçtiği için loop, biri uyurken diğerine geçiyor.
İkinci yöntem asyncio.create_task. Bunu, 'şu coroutine'i hemen arka planda başlat, sonucunu sonra alacağım' anlamında kullanın:
gorev = asyncio.create_task(veri_cek('A', 1))
# ... burada baska isler de yapabilirsiniz ...
sonuc = await gorev
Bence ikisi arasındaki ayrım net: tek bir noktada birden fazla işi paketleyip sonucunu toplu alacaksanız gather; uzun süre yaşayacak, lifecycle'ını ayrıca yöneteceğiniz arka plan işleri için create_task.
IO-bound vs CPU-bound: en kritik ayrım
Şahsi kanaatim, asyncio hakkında en sık yapılan hata şu: 'Hesabı hızlansın diye async yapayım.' Hayır, olmuyor. Asyncio sadece IO bekleyen kodu hızlandırır. Network çağrısı, disk okuma, veritabanı sorgusu, subprocess'ten cevap bekleme - bunlar IO-bound. Hepsinde Python süreyi çoğunlukla bekleyerek geçiriyor; loop bu beklemeyi başka iş yapmaya çeviriyor.
Ama tek thread'de büyük bir matrisi çarpıyorsanız, bir görüntüyü işliyorsanız, JSON parse etmiyorsanız bile saf hesap yapıyorsanız - bu CPU-bound iştir. async koymanız hiçbir şey hızlandırmaz, hatta event loop'u bloke eder ve diğer coroutine'leri açlığa sürükler. CPU-bound işin yeri multiprocessing, ya da loop.run_in_executor ile process pool.
Asenkron context manager
async with deyimi __aenter__ / __aexit__ metotlarına sahip nesneler bekler. Veritabanı bağlantısı, HTTP session, dosya gibi kaynakların asenkron açılıp kapanması için doğal yol bu:
from contextlib import asynccontextmanager
@asynccontextmanager
async def baglanti(dsn: str):
print(f'baglaniyor: {dsn}')
await asyncio.sleep(0.1)
try:
yield {'dsn': dsn}
finally:
print('kapaniyor')
await asyncio.sleep(0.1)
async def main():
async with baglanti('postgres://localhost/app') as conn:
print(conn)
asynccontextmanager dekoratörü, küçük yardımcılar için sınıf yazma zahmetinden kurtarıyor.
Sık karşılaşılan hatalar
- Threading ile karıştırmak: asyncio thread değildir.
time.sleep(5)yazarsanız tüm event loop bloke olur, çünkü o satır loop'a kontrolü vermez. Bekleme için her zamanawait asyncio.sleep(...)ya da loop-aware bir kütüphane kullanın. - Senkron kütüphane kullanmak:
requests,psycopg2,sqlite3gibi paketler bloklayıcı IO yapar. async fonksiyon içinde çağırırsanız hiçbir şey 'paralel' olmaz.httpx,aiohttp,asyncpggibi asenkron eşdeğerleri tercih edin; mecbursanızloop.run_in_executorile thread pool'a atın. asyncio.run'u iç içe çağırmak: Bir loop zaten çalışırkenasyncio.runçağırırsanızRuntimeErroralırsınız. Mevcut loop varsaawaitya dacreate_taskkullanın.gatherçağrısında istisnayı görmezden gelmek: Varsayılanda bir coroutine fail edersegatherilk hatayı yükseltir; diğer sonuçları kaybedebilirsiniz. Hepsini görmek içinreturn_exceptions=Trueverip her sonucu tek tek kontrol edin.
Kapanış
Bu yazıda Python'da async/await'in nasıl çalıştığına, gather ile create_task'in farkına ve threading'den neden ayrı bir hayvan olduğuna baktık. Bana sorarsanız asyncio'yu hayatınıza sokmanın doğru sırası şu: önce IO-bound mu CPU-bound mu olduğuna karar verin, sonra senkron kütüphanelerinizi loop-aware muadilleriyle değiştirin, ondan sonra gather'a geçin. Umarım faydalı olur, bir sonraki yazıda görüşmek üzere.
