Məzmuna keç
Educora
İrəli22 dəq21 / 42

Asinxron Python: async və await

Gözləmə tələb edən işləri eyni vaxtda icra et: korutinlər, `await`, hadisə dövrü, `asyncio.run`, `gather`, `create_task`, `TaskGroup`, vaxt limitləri, `async for` və hadisə dövrünü bloklamağın təhlükələri.

Özünü yoxla
Bu dərsdə öyrənəcəksən
  • Korutin, await və hadisə dövrü anlayışlarını izah etmək
  • gather, create_task və TaskGroup ilə korutinləri eyni vaxtda işlətmək
  • wait_for ilə vaxt limiti qoymaq və async for işlətmək
  • asyncio-nun nə vaxt fayda verdiyini və hadisə dövrünü nəyin blokladığını bilmək

Proqram internetdən 100 səhifə yükləməlidir. Hər sorğu təxminən 0,3 saniyəni sadəcə şəbəkəni gözləməyə sərf edir — bu müddətdə prosessor heç nə etmir. Ardıcıl işləsək, bu, 30 saniyəlik, əsasən boş gözləmədir. Çayxanadakı ofisiantı xatırla: o, bir sifariş hazır olana qədər mətbəxin qapısında dayanmır, bu arada növbəti masanın sifarişini götürür. Asinxron proqramlaşdırma bir axının məhz belə işləməsinə imkan verir: bir əməliyyat gözləyərkən digərləri irəliləyir.

Korutinlər, await və hadisə dövrü

Tərif
Korutin (coroutine)

async def ilə təyin olunan funksiya korutin funksiyasıdır. Onu çağırmaq gövdəni icra etmir — generator funksiyası kimi o da korutin obyekti qaytarır və bu obyekti await etmək lazımdır.

await korutini icra edir və nəticəsini gözləyir. Hələ bitməmiş əməliyyatın hər await-ində korutin dayanır və idarəni hadisə dövrünə (event loop) qaytarır — hansı dayanmış korutinin davam edəcəyinə qərar verən planlaşdırıcıya. Heç nə paralel işləmir: axın birdir, korutinlər isə növbəni könüllü olaraq bir-birinə verir. Buna kooperativ çoxməsələlilik deyilir.

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)
▸ Gözlənilən nəticə
coroutine
Hello, Aysel!
AYSEL
Brauzerdə bu kod artıq işləyən hadisə dövrünün içində icra olunur, ona görə await-i birbaşa ən üst səviyyədə yazmaq olar.

Adi .py faylında funksiyadan kənar await sintaksis xətasıdır. Orada proqramın adətən main() adlanan bir giriş korutini olur və hadisə dövrü skriptin lap sonunda, bir dəfə **asyncio.run(main())** ilə işə salını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())
Gözlənilən nəticə
['rates (0.3 s)', 'news (0.1 s)']
Adi skriptin quruluşu: kompüterində python app.py ilə işə sal.

gather ilə eyni vaxtda icra

asyncio.gather(*coroutines) bir neçə korutini birdən başladır və hamısı bitənə qədər gözləyir. Sətirlərin ardıcıllığına bax: hər üçü dərhal başlayır, yazılış sırasına görə yox, gözləmə müddətinə görə bitir. Nəticələr isə arqumentlərin sırası ilə qayıdır:

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)
▸ Gözlənilən nəticə
start rates
start weather
start news
done  news
done  weather
done  rates
['rates (0.3 s)', 'weather (0.2 s)', 'news (0.1 s)']

İndi ölçək. Korutinləri bir-birinin ardınca gözləsək, gözləmələr toplanır; gather ilə isə onlar üst-üstə düşür, ona görə ümumi vaxt ən uzun gözləməyə bərabər 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')
Təxminən 0,60 və 0,30 saniyə gözlə; dəqiq rəqəmlər hər dəfə bir az fərqlənir.

Tapşırıqlar, TaskGroup və vaxt limitləri

asyncio.create_task(coro) korutini dərhal arxa planda işləmək üçün planlaşdırır və tapşırıq (task) qaytarır. Bu arada başqa işlər görə, nəticəni isə sonra tapşırığı await edərək ala bilərsən. Python 3.11-dən bəri **asyncio.TaskGroup** strukturlaşdırılmış eyni vaxtda icra təklif edir: async with bloku bütün tapşırıqları bitənə qədər tamamlanmır, onlardan biri xəta versə, qalanları avtomatik ləğv olunur:

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()
▸ Gözlənilən nəticə
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

Şəbəkə xidmətləri bəzən heç cavab vermir. asyncio.wait_for(coro, timeout) verilmiş saniyələrdən sonra əməliyyatı ləğv edir və TimeoutError qaldırır (Python 3.11-dən bəri asyncio.TimeoutError eyni sinifdir):

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')
▸ Gözlənilən nəticə
the API did not answer in 0.2 s

Dilin başqa konstruksiyalarının da asinxron variantları var: hazırlanması və ya təmizlənməsi gözləmə tələb edən kontekst menecerləri (məsələn, şəbəkə əlaqəsi) üçün async with, asinxron iteratorlar üçün isə async for. Asinxron generator — içində yield olan async def funksiyasıdır:

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)
▸ Gözlənilən nəticə
tick 1
tick 2
tick 3
[1, 4, 9, 16]

Tələlər və asyncio-nu nə vaxt seçməli

YanaşmaNə üçün yaxşıdırNümunə
asynciobir axında çoxlu eyni vaxtlı gözləmə (şəbəkə, soketlər)çat serveri, veb-krauler, FastAPI
axınlar (threading)await edilə bilməyən bloklayan kitabxanalarrequests ilə bir neçə yükləmə
proseslər (multiprocessing)bütün prosessor nüvələrində ağır hesablamalarşəkillərin emalı, simulyasiyalar
Tapşırıq

Üç yeməyi asyncio.gather ilə eyni vaxtda bişir və nəticələr siyahısını çap et. Diqqət et: «ready» sətirləri bişmə müddətinə görə çıxır, siyahı isə menyu sırasını saxlayır.

Tapşırıq · 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
▸ Gözlənilən nəticə
ready: salad
ready: soup
ready: plov
['plov', 'salad', 'soup']
Tapşırıq

fetch_with_limit korutinini yaz: o, fetch-i limit saniyəlik vaxt limiti ilə işə salsın və nəticəni, vaxt bitəndə isə '<name>: timeout' sətrini qaytarsın.

Tapşırıq · 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)
▸ Gözlənilən nəticə
['fast: ok', 'slow: timeout']

Əsas fikirlər

  • async def korutin təyin edir; onu çağırmaq korutin obyekti qaytarır, await isə onu icra edir.
  • Hər await-də korutin dayanır və hadisə dövrü başqasını işə salır — bir axın, kooperativ çoxməsələlilik.
  • Skript asyncio.run(main()) ilə başlayır; brauzerdə və Jupyter-də birbaşa await yazılır.
  • gather, create_task və TaskGroup korutinləri eyni vaxtda işlədir; ümumi vaxt ≈ ən uzun gözləmə.
  • Dövrü heç vaxt time.sleep və ya ağır hesablama ilə bloklama; asyncio gözləmə (I/O) tapşırıqları üçündür.

Özünü yoxla

10 sual. Hər düzgün cavab XP qazandırır.

1 / 10
async def load(): ... təyin olunub. load() çağırışı nə qaytarır?