İçeriğe geç
Educora
İleri22 dk21 / 42

Asenkron Python: async ve await

Bekleme gerektiren işleri eşzamanlı çalıştır: eşyordamlar, `await`, olay döngüsü, `asyncio.run`, `gather`, `create_task`, `TaskGroup`, zaman aşımları, `async for` ve döngüyü engellemenin tehlikeleri.

Kendini test et
Bu derste öğreneceklerin
  • Eşyordam, await ve olay döngüsü kavramlarını açıklamak
  • gather, create_task ve TaskGroup ile eşyordamları eşzamanlı çalıştırmak
  • wait_for ile zaman aşımı koymak ve async for kullanmak
  • asyncio'nun ne zaman işe yaradığını ve olay döngüsünü neyin engellediğini bilmek

Bir programın internetten 100 sayfa indirmesi gerekiyor. Her istek yaklaşık 0,3 saniyesini yalnızca ağı beklemekle geçirir; bu sürede işlemci hiçbir şey yapmaz. Sırayla yapılırsa bu, çoğu boş geçen 30 saniyelik bir beklemedir. Bir çay bahçesindeki garsonu düşün: bir sipariş hazırlanana kadar mutfak kapısında dikilmez, bu arada sonraki masanın siparişini alır. Asenkron programlama, tek bir iş parçacığının da böyle çalışmasını sağlar: bir işlem beklerken diğerleri ilerler.

Eşyordamlar, await ve olay döngüsü

Tanım
Eşyordam (coroutine)

async def ile tanımlanan fonksiyon bir eşyordam fonksiyonudur. Onu çağırmak gövdesini çalıştırmaz; tıpkı bir üreteç fonksiyonu gibi bir eşyordam nesnesi döndürür ve bu nesnenin await ile beklenmesi gerekir.

await bir eşyordamı çalıştırır ve sonucunu bekler. Henüz bitmemiş bir işlemdeki her await'te eşyordam duraklar ve denetimi olay döngüsüne (event loop) geri verir; olay döngüsü, duraklamış eşyordamlardan hangisinin devam edeceğine karar veren zamanlayıcıdır. Hiçbir şey paralel çalışmaz: tek bir iş parçacığı vardır ve eşyordamlar sırayı gönüllü olarak birbirine bırakır. Buna işbirlikçi çoklu görev (cooperative multitasking) denir.

Python
import asyncio

async def greet(name, delay):
    await asyncio.sleep(delay)
    print(f'Hello, {name}!')
    return name.upper()

coro = greet('Aysel', 0.1)
print(type(coro).__name__)
result = await coro
print(result)
▸ Beklenen çıktı
coroutine
Hello, Aysel!
AYSEL
Tarayıcıda bu kod zaten çalışan bir olay döngüsünün içinde yürütülür; bu yüzden await doğrudan en üst düzeyde yazılabilir.

Sıradan bir .py dosyasında fonksiyon dışındaki await bir sözdizimi hatasıdır. Orada programın genellikle main() adlı tek bir giriş eşyordamı olur ve olay döngüsü betiğin en sonunda bir kez **asyncio.run(main())** ile başlatılır:

Python
import asyncio

async def fetch(name, seconds):
    await asyncio.sleep(seconds)
    return f'{name} ({seconds} s)'

async def main():
    results = await asyncio.gather(fetch('rates', 0.3), fetch('news', 0.1))
    print(results)

if __name__ == '__main__':
    asyncio.run(main())
Beklenen çıktı
['rates (0.3 s)', 'news (0.1 s)']
Sıradan bir betiğin yapısı: bilgisayarında python app.py ile çalıştır.

gather ile eşzamanlı çalıştırma

asyncio.gather(*coroutines) birkaç eşyordamı aynı anda başlatır ve hepsi bitene kadar bekler. Satırların sırasına bak: üçü de hemen başlar ve yazılış sırasına göre değil, bekleme sürelerine göre biter. Sonuçlar ise argümanların sırasıyla geri döner:

Python
import asyncio

async def fetch(name, seconds):
    print(f'start {name}')
    await asyncio.sleep(seconds)
    print(f'done  {name}')
    return f'{name} ({seconds} s)'

results = await asyncio.gather(
    fetch('rates', 0.3),
    fetch('weather', 0.2),
    fetch('news', 0.1),
)
print(results)
▸ Beklenen çıktı
start rates
start weather
start news
done  news
done  weather
done  rates
['rates (0.3 s)', 'weather (0.2 s)', 'news (0.1 s)']

Şimdi ölçelim. Eşyordamlar birbiri ardına beklenirse beklemeler toplanır; gather ile ise üst üste biner, bu yüzden toplam süre en uzun beklemeye eşit olur:

Python
import asyncio
import time

async def fetch(seconds):
    await asyncio.sleep(seconds)

start = time.perf_counter()
for s in (0.3, 0.2, 0.1):
    await fetch(s)
print(f'one after another: {time.perf_counter() - start:.2f} s')

start = time.perf_counter()
await asyncio.gather(fetch(0.3), fetch(0.2), fetch(0.1))
print(f'with gather:       {time.perf_counter() - start:.2f} s')
Yaklaşık 0,60 ve 0,30 saniye bekle; kesin sayılar her çalıştırmada biraz farklıdır.

Görevler, TaskGroup ve zaman aşımları

asyncio.create_task(coro), bir eşyordamı hemen arka planda çalışmak üzere zamanlar ve bir görev (task) döndürür. Bu arada başka işler yapabilir, sonucu ise daha sonra görevi await ederek alabilirsin. Python 3.11'den beri **asyncio.TaskGroup** yapılandırılmış eşzamanlılık sunar: async with bloğu tüm görevleri bitmeden sona ermez ve biri hata verirse diğerleri otomatik olarak iptal edilir:

Python
import asyncio

async def worker(name, seconds):
    await asyncio.sleep(seconds)
    print(f'{name} finished')
    return seconds

async def main():
    task = asyncio.create_task(worker('backup', 0.2))
    print('task created, doing other work...')
    await asyncio.sleep(0.1)
    print('other work done, task done?', task.done())
    print('result:', await task)

    async with asyncio.TaskGroup() as tg:
        t1 = tg.create_task(worker('A', 0.2))
        t2 = tg.create_task(worker('B', 0.1))
    print('group results:', t1.result(), t2.result())

await main()
▸ Beklenen çıktı
task created, doing other work...
other work done, task done? False
backup finished
result: 0.2
B finished
A finished
group results: 0.2 0.1

Ağ hizmetleri bazen hiç yanıt vermez. asyncio.wait_for(coro, timeout), verilen saniye sonunda işlemi iptal eder ve TimeoutError fırlatır (Python 3.11'den beri asyncio.TimeoutError aynı sınıftır):

Python
import asyncio

async def slow_api():
    await asyncio.sleep(1)
    return 'data'

try:
    result = await asyncio.wait_for(slow_api(), timeout=0.2)
except TimeoutError:
    print('the API did not answer in 0.2 s')
▸ Beklenen çıktı
the API did not answer in 0.2 s

Dilin başka yapılarının da asenkron sürümleri vardır: hazırlığı ya da temizliği bekleme gerektiren bağlam yöneticileri (örneğin bir ağ bağlantısı) için async with, asenkron yineleyiciler için ise async for. Asenkron üreteç, içinde yield bulunan bir async def fonksiyonudur:

Python
import asyncio

async def ticker(n, delay):
    for i in range(1, n + 1):
        await asyncio.sleep(delay)
        yield i

async for value in ticker(3, 0.05):
    print('tick', value)

squares = [v * v async for v in ticker(4, 0.01)]
print(squares)
▸ Beklenen çıktı
tick 1
tick 2
tick 3
[1, 4, 9, 16]

Tuzaklar ve asyncio'yu ne zaman seçmeli

YaklaşımNe için iyidirÖrnek
asynciotek iş parçacığında çok sayıda eşzamanlı bekleme (ağ, soketler)sohbet sunucusu, web tarayıcı robotu, FastAPI
iş parçacıkları (threading)await edilemeyen engelleyici kütüphanelerrequests ile birkaç indirme
süreçler (multiprocessing)tüm işlemci çekirdeklerinde ağır hesaplamalargörüntü işleme, simülasyonlar
Alıştırma

Üç yemeği asyncio.gather ile aynı anda pişir ve sonuç listesini yazdır. Dikkat: “ready” satırları pişme süresine göre çıkar, liste ise menü sırasını korur.

Alıştırma · Python
import asyncio

async def cook(dish, seconds):
    await asyncio.sleep(seconds)
    print('ready:', dish)
    return dish

menu = [('plov', 0.3), ('salad', 0.1), ('soup', 0.2)]
# cook all dishes at the same time with asyncio.gather and print the results
▸ Beklenen çıktı
ready: salad
ready: soup
ready: plov
['plov', 'salad', 'soup']
Alıştırma

fetch_with_limit eşyordamını yaz: fetch'i limit saniyelik bir süre sınırıyla çalıştırsın ve sonucunu, süre dolarsa da '<name>: timeout' dizesini döndürsün.

Alıştırma · Python
import asyncio

async def fetch(name, seconds):
    await asyncio.sleep(seconds)
    return f'{name}: ok'

async def fetch_with_limit(name, seconds, limit):
    # return fetch's result, or f'{name}: timeout' if it takes longer than limit
    ...

results = await asyncio.gather(
    fetch_with_limit('fast', 0.05, 0.2),
    fetch_with_limit('slow', 0.5, 0.2),
)
print(results)
▸ Beklenen çıktı
['fast: ok', 'slow: timeout']

Önemli noktalar

  • async def bir eşyordam tanımlar; onu çağırmak bir eşyordam nesnesi döndürür, await ise onu çalıştırır.
  • Her await'te eşyordam duraklar ve olay döngüsü bir başkasını çalıştırır: tek iş parçacığı, işbirlikçi çoklu görev.
  • Bir betik asyncio.run(main()) ile başlar; tarayıcıda ve Jupyter'de doğrudan await yazılır.
  • gather, create_task ve TaskGroup eşyordamları eşzamanlı çalıştırır; toplam süre ≈ en uzun bekleme.
  • Döngüyü time.sleep ya da ağır hesaplamalarla engelleme; asyncio bekleme (G/Ç) görevleri içindir.

Kendini test et

10 soru. Her doğru cevap XP kazandırır.

1 / 10
async def load(): ... tanımlı. load() çağrısı ne döndürür?