Ana içeriğe geç
Kesit Python
İleri · Eşzamanlılık ve performans

asyncio: eşzamansız kod, coroutine, await ve gather

Olay döngüsü, coroutine ve await, gather/create_task ile eşzamanlı I/O, asyncio.sleep ile sahte gecikme, Semaphore ile sınırlama, timeout, async for/with.

Uygulamada aç, 18 alıştırmayı çözÜcretsiz, hesap gerekmez, kurulum yok.

Bu sayfa dersin okuma sürümüdür. Metin bir kutuyu çalıştırmanı ya da adıma geçmeni söylüyorsa dersi uygulamada aç.

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()
Olay döngüsünü başlatmakBetiklerde standart kalıp 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()
Hata yönetimiVarsayılan 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.TimeoutErrorPython 3.11'den itibaren 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 coroSonuç hemen lazım, sırayla
gatherBirkaç bağımsız iş, hepsinin sonucu lazım
create_taskArka planda başlat, sonra topla
SemaphoreEşzamanlılığı sınırla (rate limit)
wait_for / timeoutYavaş cevabı kesmek
async forParça parça gelen veri
Derinleş

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 awaitedasync 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 loopJupyter gibi döngüsü zaten çalışan ortamda asyncio.runJupyter'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' expressionSenkron fonksiyon await edildiYalnı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
TaskGroupHepsi birlikte başarılı olsun, biri düşerse hepsi dursun
gather(return_exceptions=True)Hepsini dene, hataları sonuç listesinde topla
as_completedBiten oldukça işle
wait(FIRST_COMPLETED)İlk biteni al, kalanlara karar ver
Queue + işçilerÇok iş, sabit eşzamanlılık
Lock / EventPaylaşı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()
Fabrika ver, coroutine değilYeniden deneme fonksiyonuna 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.
Gerçek işteBankacılık ve e-ticaret entegrasyonlarında async kodun büyük kısmı bu bölümdeki kalıplardan oluşur: dış sistem başına bir Semaphore (karşı tarafın hız sınırı), her çağrıya zaman aşımı, geçici hatalara üstel geri çekilmeli yeniden deneme, toplu işlerde kuyruk + işçi ve kapanışta görevlerin düzgün iptali. Bu parçalar eksik olduğunda servis ya karşı sistemi boğar ya da bir yavaş cevap yüzünden tamamen kilitlenir.
Uzmanlaş

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.

Alıştırmalara geç (18)