Beklemek zaman kaybı
Ilgaz Bank'ın bir servisi, bir kredi başvurusu için üç dış sisteme soruyor: kimlik doğrulama, kredi skoru, kara liste. Her biri yaklaşık yarım saniye. Sırayla sorarsan 1,5 saniye; aynı anda sorup üçünü de beklersen yarım saniye. İşlemci bu süre boyunca zaten boştur, yalnızca ağ cevabını bekler. asyncio bu "beklerken başka işe geç" düşüncesini tek iş parçacığında (thread) gerçekleştirir. Kural: asyncio I/O beklemesi için iyidir; ağır hesap için değil (onun için bir sonraki ders).
Coroutine ve await
async def ile tanımlanan fonksiyon çağrılınca çalışmaz, bir coroutine nesnesi döndürür. Çalışması için ya await edilir ya da olay döngüsüne (event loop) verilir. await "bu iş bitene kadar beni duraklat, döngü başka işe geçebilir" demektir. Gerçek ağ yerine asyncio.sleep ile sahte gecikme kullanacağız; time.sleep ise döngüyü bloklar, asla kullanma.
import asyncio, time
async def skor_sor(musteri: str, sure: float) -> int:
await asyncio.sleep(sure) # sahte ağ gecikmesi
return 600 + len(musteri) * 10
async def main():
t0 = time.perf_counter()
a = await skor_sor("Ayşe", 0.2) # sırayla: önce bu biter
b = await skor_sor("Mehmet", 0.2) # sonra bu
print(a, b, f"{time.perf_counter() - t0:.2f} s (sıralı)")
loop = asyncio.new_event_loop()
loop.run_until_complete(main())
loop.close()
asyncio.run(main()). Jupyter ve tarayıcı (Pyodide) gibi zaten bir döngü çalışan ortamlarda bu çağrı sorun çıkarabilir; o yüzden bu derste loop = asyncio.new_event_loop(); loop.run_until_complete(main()) kalıbını kullanıyoruz. İkisi de aynı işi yapar: coroutine'i çalıştırıp sonucunu verir.gather: hepsini aynı anda
asyncio.gather(c1, c2, c3) coroutine'leri birlikte başlatır, hepsinin bitmesini bekler ve sonuçları verilen sırayla liste olarak döndürür. Toplam süre en uzun işin süresi kadardır.
import asyncio, time
async def servis(ad: str, sure: float) -> str:
await asyncio.sleep(sure)
return f"{ad}: tamam"
async def main():
t0 = time.perf_counter()
sonuc = await asyncio.gather(
servis("kimlik", 0.3),
servis("skor", 0.2),
servis("kara_liste", 0.25),
)
print(sonuc)
print(f"{time.perf_counter() - t0:.2f} s (eşzamanlı; en uzunu 0.3)")
loop = asyncio.new_event_loop()
loop.run_until_complete(main())
loop.close()
gather'da ilk hata hemen yükselir ama diğer işler iptal edilmez, arkada çalışmaya devam eder. return_exceptions=True hatayı yükseltmez, sonuç listesinde o konuma istisna nesnesini koyar. Biri hata verince hepsini birlikte iptal etmek için asyncio.TaskGroup kullan. Toplu sorgulamada "biri patladı diye hepsi çökmesin" istiyorsan bunu kullan.create_task: arka planda başlat
gather hepsini beklerken asyncio.create_task(coro) işi hemen zamanlar ve sana bir Task verir; sen başka şey yapar, istediğinde await task ile sonucu alırsın. Bir sayaç, bir ısıtma işlemi ya da sonucu sonra gereken sorgu için uygundur.
import asyncio
async def kur_doviz(sure=0.2):
await asyncio.sleep(sure)
return {"USD": 32.5, "EUR": 35.1}
async def main():
gorev = asyncio.create_task(kur_doviz()) # hemen başlar
print("kur isteği gönderildi, başka iş yapıyorum")
await asyncio.sleep(0.05)
print("görev bitti mi?", gorev.done())
kur = await gorev # burada bekle
print("kur:", kur, "bitti mi?", gorev.done())
loop = asyncio.new_event_loop()
loop.run_until_complete(main())
loop.close()
Semaphore ve timeout
Bin müşteri için skor sorgusunu aynı anda atarsan karşı sistem seni engeller. asyncio.Semaphore(n) aynı anda en çok n işin "içeride" olmasına izin verir. asyncio.wait_for(coro, timeout=s) süre dolarsa TimeoutError fırlatır; 3.11+ ile async with asyncio.timeout(s) bloğu da var.
import asyncio, time
sem = asyncio.Semaphore(3)
async def sorgu(i: int) -> int:
async with sem: # aynı anda en çok 3
await asyncio.sleep(0.1)
return i * i
async def yavas():
await asyncio.sleep(1.0)
return "geç geldi"
async def main():
t0 = time.perf_counter()
sonuc = await asyncio.gather(*(sorgu(i) for i in range(9)))
print(sonuc, f"{time.perf_counter() - t0:.2f} s (9 iş / 3 eşzamanlı ≈ 0.3)")
try:
await asyncio.wait_for(yavas(), timeout=0.2)
except TimeoutError:
print("zaman aşımı: 0.2 s içinde cevap gelmedi")
loop = asyncio.new_event_loop()
loop.run_until_complete(main())
loop.close()
asyncio.TimeoutError yerleşik TimeoutError ile aynı sınıftır; ikisini de yakalayabilirsin. Eski sürümlerde ayrıydılar.async for ve async with
Sayfalı API'den veri çekerken her sayfa bir bekleme gerektirir; async üreteç (generator) (async def içinde yield) bunu doğal ifade eder ve async for ile tüketilir. async with ise açılış/kapanışı bekleme gerektiren kaynaklar (bağlantı, oturum) içindir; Semaphore örneğinde gördün.
import asyncio
async def sayfalar(toplam: int, boyut: int = 3):
for baslangic in range(0, toplam, boyut):
await asyncio.sleep(0.05) # her sayfa bir istek
yield list(range(baslangic, min(baslangic + boyut, toplam)))
async def main():
hepsi = []
async for sayfa in sayfalar(8):
print("sayfa geldi:", sayfa)
hepsi.extend(sayfa)
print("toplam kayıt:", len(hepsi))
loop = asyncio.new_event_loop()
loop.run_until_complete(main())
loop.close()
| Araç | Ne zaman |
|---|---|
await coro | Sonuç hemen lazım, sırayla |
gather | Birkaç bağımsız iş, hepsinin sonucu lazım |
create_task | Arka planda başlat, sonra topla |
Semaphore | Eşzamanlılığı sınırla (rate limit) |
wait_for / timeout | Yavaş cevabı kesmek |
async for | Parça parça gelen veri |
Derinleş: yapılandırılmış eşzamanlılık, iptal, kuyruk ve dayanıklılık
gather, create_task, Semaphore ve wait_for ile birden çok işi aynı anda yürütebiliyorsun. Üretimdeki bir async servis ise daha fazlasını ister: görevlerin hata durumunda birlikte iptal edilmesi, ilk gelen cevabı alıp diğerlerini durdurmak, iptal edilen işin arkasını temizlemesi, sabit sayıda işçiyle kuyruk işlemek, paylaşılan durumu korumak ve geçici hatalarda yeniden denemek. Bu bölüm bu kalıpları tamamlıyor.
Kendi bilgisayarında: kurulum ve sık hatalar
asyncio standart kütüphanededir, kurulum gerekmez. Gerçek HTTP istekleri için async destekli bir istemci kurarsın; requests senkrondur ve olay döngüsünü bloklar.
python -m pip install httpx # ya da: aiohttp
import asyncio, httpx
async def main():
async with httpx.AsyncClient(timeout=5) as istemci:
cevaplar = await asyncio.gather(
*(istemci.get(f"https://api.ornek.com/musteri/{i}") for i in range(10)))
print([c.status_code for c in cevaplar])
asyncio.run(main()) # betikte standart giriş noktası
| Hata / uyarı | Neden | Çözüm |
|---|---|---|
RuntimeWarning: coroutine '...' was never awaited | async fonksiyon await edilmeden çağrıldı | Başına await ekle ya da create_task ile zamanla |
RuntimeError: asyncio.run() cannot be called from a running event loop | Jupyter gibi döngüsü zaten çalışan ortamda asyncio.run | Jupyter'de doğrudan await main() yaz |
Task was destroyed but it is pending! | Program, görev bitmeden ya da iptal edilmeden kapandı | Görevleri TaskGroup içinde tut ya da kapanışta iptal edip bekle |
TypeError: object NoneType can't be used in 'await' expression | Senkron fonksiyon await edildi | Yalnızca coroutine, Task ve Future await edilir |
| Servis "donuyor" | Döngüde time.sleep ya da senkron ağ/veritabanı çağrısı | Async kütüphane ya da asyncio.to_thread |
TaskGroup: birlikte başla, birlikte bit
async with asyncio.TaskGroup() as tg: bloğunda tg.create_task(...) ile açılan tüm görevler blok bitmeden beklenir. Bir görev hata verirse grup diğerlerini iptal eder ve hataları ExceptionGroup içinde fırlatır; except* ile türüne göre yakalanır. gather'dan farkı, "biri patladı ama diğerleri arka planda çalışmaya devam ediyor" durumunun olmamasıdır.
import asyncio
async def sorgu(sistem: str, sure: float, hata: bool = False) -> str:
await asyncio.sleep(sure)
if hata:
raise ConnectionError(f"{sistem} cevap vermedi")
return f"{sistem}: tamam"
async def main():
async with asyncio.TaskGroup() as tg:
t1 = tg.create_task(sorgu("kimlik", 0.02))
t2 = tg.create_task(sorgu("skor", 0.01))
print(t1.result(), "|", t2.result())
try:
async with asyncio.TaskGroup() as tg:
yavas = tg.create_task(sorgu("kara_liste", 0.5))
tg.create_task(sorgu("skor", 0.01, hata=True))
except* ConnectionError as eg:
print("hata:", eg.exceptions[0])
print("yavaş görev iptal edildi mi?", yavas.cancelled())
loop = asyncio.new_event_loop()
loop.run_until_complete(main())
loop.close()
İlk biteni almak: as_completed ve wait
asyncio.as_completed(gorevler) sonuçları bitiş sırasına göre verir; uzun süren bir listede ilk gelenleri ekrana basmak için idealdir. asyncio.wait(gorevler, return_when=asyncio.FIRST_COMPLETED) ise (bitenler, bekleyenler) kümelerini döndürür; aynı veriyi üç yedek sunucudan isteyip en hızlısını almak ve diğerlerini iptal etmek tipik kullanımdır.
import asyncio
async def kur(kaynak: str, sure: float) -> str:
await asyncio.sleep(sure)
return kaynak
async def main():
for gelecek in asyncio.as_completed([kur("A", 0.05), kur("B", 0.01), kur("C", 0.03)]):
print("geldi:", await gelecek)
gorevler = [asyncio.create_task(kur(k, s)) for k, s in [("ana", 0.2), ("yedek", 0.02)]]
biten, bekleyen = await asyncio.wait(gorevler, return_when=asyncio.FIRST_COMPLETED)
print("en hızlı:", biten.pop().result(), "| bekleyen:", len(bekleyen))
for g in bekleyen:
g.cancel()
await asyncio.gather(*bekleyen, return_exceptions=True) # iptalin bitmesini bekle
loop = asyncio.new_event_loop()
loop.run_until_complete(main())
loop.close()
İptal (cancellation) ve temizlik
task.cancel() görevin beklediği noktaya CancelledError fırlatır. Görev try/finally ile bağlantıyı kapatmak, geçici dosyayı silmek gibi temizliği yapabilir. CancelledError'ı yakalayıp yutma: except Exception onu yakalamaz (BaseException alt sınıfıdır) ama çıplak except: yakalar ve iptal sessizce bozulur. Yakalarsan temizlikten sonra raise ile yeniden fırlat.
Kuyruk ve sabit işçi havuzu
10.000 işi tek seferde gather'a vermek 10.000 görev nesnesi demektir. asyncio.Queue ile N işçi kurup işleri kuyruktan çektirmek bellek ve eşzamanlılığı birlikte sınırlar. Her iş bittiğinde task_done(), üretici tarafında await kuyruk.join() "tüm işler işlendi" anını verir; sonra sonsuz döngüdeki işçiler iptal edilir.
import asyncio
async def isci(ad: str, kuyruk: asyncio.Queue, sonuc: list):
while True:
siparis = await kuyruk.get()
try:
await asyncio.sleep(0.01) # kargo API çağrısı
sonuc.append((siparis, ad))
finally:
kuyruk.task_done()
async def main():
kuyruk = asyncio.Queue()
for no in range(1, 9):
kuyruk.put_nowait(no)
sonuc = []
iscilar = [asyncio.create_task(isci(f"i{k}", kuyruk, sonuc)) for k in range(3)]
await kuyruk.join() # hepsi işlendi
for t in iscilar:
t.cancel()
await asyncio.gather(*iscilar, return_exceptions=True)
print(len(sonuc), "sipariş,", len({ad for _, ad in sonuc}), "işçi kullandı")
loop = asyncio.new_event_loop()
loop.run_until_complete(main())
loop.close()
Tek iş parçacığında da yarış olur: asyncio.Lock
asyncio tek iş parçacığında çalışır, ama her await bir geçiş noktasıdır. Bakiyeyi okuyup await edip sonra yazarsan, araya giren başka bir görev aynı eski değeri okur ve bir güncelleme kaybolur. Okuma-bekleme-yazma bloğunu async with kilit: içine al. Bir görevin diğerine "hazırım" demesi için asyncio.Event kullanılır.
| Araç | Soru |
|---|---|
TaskGroup | Hepsi birlikte başarılı olsun, biri düşerse hepsi dursun |
gather(return_exceptions=True) | Hepsini dene, hataları sonuç listesinde topla |
as_completed | Biten oldukça işle |
wait(FIRST_COMPLETED) | İlk biteni al, kalanlara karar ver |
Queue + işçiler | Çok iş, sabit eşzamanlılık |
Lock / Event | Paylaşılan durum / görevler arası sinyal |
Yeniden deneme ve üstel geri çekilme
Ağ hataları çoğu zaman geçicidir. Doğru tepki, hemen tekrar saldırmak değil, giderek artan aralıklarla (0.1, 0.2, 0.4 s; üstel geri çekilme, exponential backoff) birkaç kez denemek ve sonunda hatayı yukarı iletmektir. Yalnızca geçici hatalarda (bağlantı, zaman aşımı) yeniden dene; "yetkisiz" ya da "geçersiz istek" gibi kalıcı hataları tekrarlamak işe yaramaz.
import asyncio
deneme_sayisi = 0
async def kararsiz_servis() -> str:
global deneme_sayisi
deneme_sayisi += 1
await asyncio.sleep(0)
if deneme_sayisi < 3:
raise ConnectionError("geçici hata")
return "skor: 712"
async def tekrar_dene(fabrika, deneme=4, bekleme=0.01):
for i in range(deneme):
try:
return await fabrika()
except ConnectionError as e:
if i == deneme - 1:
raise
sure = bekleme * 2 ** i
print(f"{i + 1}. deneme başarısız ({e}), {sure:.2f} s bekleniyor")
await asyncio.sleep(sure)
async def main():
print(await tekrar_dene(kararsiz_servis))
loop = asyncio.new_event_loop()
loop.run_until_complete(main())
loop.close()
kararsiz_servis() (coroutine nesnesi) verirsen ikinci denemede cannot reuse already awaited coroutine hatası alırsın: bir coroutine yalnızca bir kez await edilebilir. Fonksiyonun kendisini (kararsiz_servis) ya da lambda: servis(id) ver.Bloklayan kodu izole et
Olay döngüsünde time.sleep, ağır hesap ya da senkron veritabanı sürücüsü tüm görevleri dondurur. Böyle bir işi await asyncio.to_thread(func, *args) ile ayrı iş parçacığına at; döngü akmaya devam eder. Gerçek ağ kütüphanesi seçerken async destekli olanı seç (httpx, aiohttp, asyncpg).
Task yaşam döngüsü
Referansı tutulmayan bir Task çöp toplayıcı tarafından yok edilebilir; task'leri bir listede ya da asyncio.TaskGroup (3.11+) içinde tut. TaskGroup, async with bloğu bitene kadar tüm görevleri bekler ve biri hata verirse diğerlerini iptal eder; yapılandırılmış eşzamanlılık (structured concurrency) denilen bu yaklaşım üretim kodunda tercih edilir.
Ne zaman async?
Binlerce eşzamanlı bağlantı, web sunucusu (FastAPI), çok sayıda API çağrısı: evet. Tek dosya işleme, CPU-yoğun analiz, basit betik: hayır; karmaşıklığı hak etmez.