அடிப்படை

Python-இல் Asyncio: async/await, TaskGroup, இழைகள், multiprocessing மற்றும் GIL

புதுப்பிக்கப்பட்டது: · Skillaria.Top

Asyncio என்பது Python-இன் உள்ளமைவு நூலகம்: async def செயல்பாடுகள் (coroutine-கள்) await-இல் இடைநிறுத்தப்படுகின்றன, அப்போது நிகழ்வுச் சுழற்சி (event loop) மற்ற பணிகளை இயக்குகிறது; அதனால் நூற்றுக்கணக்கான பிணையக் கோரிக்கைகள் ஒரே இழையில் ஒரே நேரத்தில் ஓடுகின்றன. பிணையம் அல்லது வட்டுக்காகக் காத்திருக்க asyncio அல்லது இழைகள் (threading) ஏற்றவை; கனமான கணக்கீடுகளுக்கு செயல்முறைகள் (multiprocessing) — ஏனெனில் சாதாரண CPython-இல் GIL இழைகளை ஒரே நேரத்தில் கணக்கிட விடுவதில்லை.

Asyncio என்பது Python-இன் ஒத்திசைவற்ற குறியீட்டுக்கான உள்ளமைவு நூலகம். async def மூலம் அறிவிக்கப்பட்ட செயல்பாடுகள் await-இல் இடைநிறுத்தப்படலாம்; ஒரு பணி பிணைய பதிலுக்காகக் காத்திருக்கும்போது நிகழ்வுச் சுழற்சி மற்றவற்றை இயக்கும். அதனால் நூற்றுக்கணக்கான கோரிக்கைகள் ஒரே இழையில் ஒரே நேரத்தில் செல்கின்றன. ஆனால் asyncio மூன்று கருவிகளில் ஒன்று மட்டுமே: இன்னும் இழைகள் (threading) மற்றும் செயல்முறைகள் (multiprocessing) உண்டு. asyncio python எப்படி வேலை செய்கிறது, உங்கள் பணிக்கு எதைத் தேர்ந்தெடுப்பது, GIL-க்கு இதில் என்ன பங்கு என்று பார்ப்போம்.

காத்திருப்பதும் கணக்கிடுவதும் வேறு வேலைகள்

இந்த வேறுபாடு இல்லாமல் மேலே எதையும் புரிந்துகொள்ள முடியாது. நிரல் இரண்டு காரணங்களில் ஒன்றால் மெதுவாகிறது:

  • I/O-bound — உள்ளீடு-வெளியீட்டுக்காகக் காத்திருக்கிறது: API பதில், தரவுத்தளம், வட்டிலிருந்து படித்தல். அந்நேரம் செயலி சும்மா இருக்கும்.
  • CPU-bound — கணக்கிடுகிறது: படங்களைச் செயலாக்குவது, மில்லியன் கணக்கான பதிவுகளின் மீது சுழல்கள். செயலி முழு வேகத்தில் உழைக்கும்.

காத்திருப்புக்கு asyncio அல்லது இழைகள் பொருந்தும்: ஒரு செயல்பாடு காத்திருக்கும்போது மற்றொன்று ஓடும். கணக்கீடுகளுக்கு பல கோர்களில் உண்மையான இணைகணக்கீடு தேவை — அதற்கு செயல்முறைகள். இவற்றைக் குழப்புவதே முக்கியப் பிழை: சுத்தமான Python-இல் இழைகள் கணக்கீட்டை வேகப்படுத்தாது; மூன்று HTTP கோரிக்கைகளுக்கு செயல்முறைகளை எடுப்பது சிட்டுக்குருவியை சுட பீரங்கியைப் பயன்படுத்துவது.

Asyncio என்றால் என்ன: coroutine-களும் நிகழ்வுச் சுழற்சியும்

ஒத்திசைவற்ற குறியீடு இரண்டு சொற்களின் மேல் கட்டப்பட்டுள்ளது. async def ஒரு coroutine-ஐ அறிவிக்கிறது — இடைநிறுத்தக்கூடிய செயல்பாடு. அதை அழைப்பது உடலை இயக்காது; coroutine பொருளை உருவாக்குகிறது. உள்ளே உள்ள await சொல்கிறது: "இங்கே நான் காத்திருப்பேன், நீ மற்ற பணிகளைப் பார்". அனைத்தையும் நிகழ்வுச் சுழற்சி (event loop) நிர்வகிக்கிறது; அது asyncio.run() மூலம் தொடங்கப்படுகிறது.

import asyncio

async def fetch_rate(currency):
    await asyncio.sleep(1)          # API கோரிக்கையின் போலி வடிவம்
    return {"USD": 92.5, "EUR": 100.1, "CNY": 12.7}[currency]

async def main():
    rate = await fetch_rate("USD")
    print(rate)

asyncio.run(main())  # 92.5

asyncio.sleep() ஒத்திசைவற்ற இடைவேளை: அது கட்டுப்பாட்டை நிகழ்வுச் சுழற்சிக்குத் தருகிறது. Coroutine-க்குள் சாதாரண time.sleep() முழு சுழற்சியையும் தடுக்கும்; மற்ற பணிகள் நின்றுவிடும்.

ஒன்றன் பின் ஒன்றா, ஒரே நேரத்திலா

சுழலில் await எழுதினால் கோரிக்கைகள் ஒவ்வொன்றாகச் செல்லும் — மூன்று நாணயங்களுக்கு மூன்று வினாடிகள் ஆகும். அவை ஒரே நேரத்தில் செல்ல வேண்டுமென்றால் பணிகளாகத் தொடங்க வேண்டும்.

import asyncio, time

async def main():
    start = time.perf_counter()
    for cur in ("USD", "EUR", "CNY"):
        await fetch_rate(cur)                       # ஒவ்வொன்றாக
    print(f"{time.perf_counter() - start:.1f} வி")   # 3.0 வி

asyncio.run(main())

TaskGroup: ஒன்றாகத் தொடங்கி எல்லாவற்றுக்காகவும் காத்திருப்பது

நவீன வழி (Python 3.11+) — பணிக் குழு. async with தொகுதிக்குள் ஒவ்வொரு பணியும் tg.create_task() மூலம் உடனே தொடங்கும்; எல்லாம் முடிந்ததும்தான் தொகுதியிலிருந்து வெளியேறுவோம்.

async def main():
    start = time.perf_counter()
    async with asyncio.TaskGroup() as tg:
        tasks = {cur: tg.create_task(fetch_rate(cur)) for cur in ("USD", "EUR", "CNY")}
    rates = {cur: t.result() for cur, t in tasks.items()}
    print(rates, f"{time.perf_counter() - start:.1f} வி")   # ... 1.0 வி

asyncio.run(main())

TaskGroup கண்டிப்பாக வேலை செய்கிறது: ஒரு பணி விழுந்தால் மற்றவை ரத்து செய்யப்படும், எல்லாப் பிழைகளும் ExceptionGroup-ஆகச் சேகரிக்கப்படும். அவற்றை except* மூலம் பிடிக்கிறோம்:

async def main():
    try:
        async with asyncio.TaskGroup() as tg:
            tg.create_task(fetch_rate("USD"))
            tg.create_task(fetch_rate("XXX"))   # KeyError
    except* KeyError as eg:
        print("தெரியாத நாணயம்:", eg.exceptions)

asyncio.gather மற்றும் create_task

TaskGroup-க்கு முன் நிலையான வழி asyncio.gather() ஆக இருந்தது: அது coroutine-களை ஒரே நேரத்தில் இயக்கி, முடிவுகளை அதே வரிசையில் திருப்பும். இன்றும் ஏற்கனவே உள்ள குறியீட்டில் அது நிறைய உள்ளது.

async def main():
    usd, eur, cny = await asyncio.gather(
        fetch_rate("USD"), fetch_rate("EUR"), fetch_rate("CNY")
    )
    # return_exceptions=True எனில் பிழைகள் பட்டியலில் திரும்பும், எல்லாவற்றையும் நிறுத்தாது

asyncio.create_task() ஒரு coroutine-ஐ குழு இல்லாமல் பின்னணியில் தொடங்குகிறது. பணிக்கான குறிப்பைச் சேமிக்க வேண்டும்; இல்லையென்றால் குப்பை சேகரிப்பான் அதை நீக்கிவிடலாம், பிழைகளையும் யாரும் பார்க்க மாட்டார்கள். அதனால் புதிய திட்டங்களுக்கு TaskGroup நம்பகமானது.

பணிகளை ரத்து செய்தல் மற்றும் நேர வரம்புகள்

async def main():
    task = asyncio.create_task(fetch_rate("USD"))
    task.cancel()                     # பணிக்குள் CancelledError எழும்
    try:
        await task
    except asyncio.CancelledError:
        print("ரத்து செய்யப்பட்டது")

    try:
        async with asyncio.timeout(0.5):   # Python 3.11+
            await fetch_rate("EUR")
    except TimeoutError:
        print("மிக நேரம் ஆனது")

பழைய பதிப்புகளில் asyncio.timeout()-க்குப் பதிலாக asyncio.wait_for(coro, timeout=0.5) பயன்படுத்தப்படுகிறது. இன்னும் asyncio.wait() உண்டு — அது முடிந்த மற்றும் முடியாத பணிகளின் தொகுப்புகளைத் தரும்; முதலில் முடிவதற்காகக் காத்திருக்கவும் முடியும் (return_when=asyncio.FIRST_COMPLETED).

பாடநெறியின்படி Python 3.15-இல் TaskGroup-க்கு tg.cancel() முறை வந்தது: அது குழுவின் எல்லாப் பணிகளையும் விதிவிலக்கின்றி ரத்து செய்கிறது. இது "யார் முதலில்" சூழ்நிலையைத் தீர்க்கிறது: மூன்று மாற்று சேவைகளிடம் கேட்டு, முதல் பதிலை எடுத்து, மற்றவற்றை ரத்து செய்வது. முன்பு இதற்கு தனிச் சமிக்ஞை விதிவிலக்கை எழுப்ப வேண்டியிருந்தது அல்லது பணிகளை கைமுறையாக ரத்து செய்ய வேண்டியிருந்தது.

ஒத்திசைவு: Semaphore, Lock, Event, Queue

asyncio-க்கு தனியான கருவிகள் உள்ளன; அவை await-உடன் வேலை செய்கின்றன. மிகப் பயனுள்ளது Semaphore: ஒரேநேர செயல்பாடுகளின் எண்ணிக்கையைக் கட்டுப்படுத்துகிறது — பிறர் API-ஐ ஆயிரம் கோரிக்கைகளால் வீழ்த்தாமல் இருக்க.

sem = asyncio.Semaphore(10)

async def limited(url):
    async with sem:              # ஒரே நேரத்தில் 10-க்கு மேல் இல்லை
        await asyncio.sleep(0.1) # இங்கே கோரிக்கை இருக்கும்
        return url
  • asyncio.Lock — முக்கியப் பகுதிக்குள் ஒரே ஒரு coroutine மட்டும்.
  • asyncio.Event — ஒரு coroutine await event.wait()-க்காகக் காத்திருக்கும், மற்றொன்று event.set() மூலம் சமிக்ஞை செய்யும்.
  • asyncio.Queue — தயாரிப்பாளர்–நுகர்வோர் வரிசை: await q.put(), await q.get(), q.task_done().

பொறி: await மறந்துவிட்டது

மிகவும் பொதுவான தவறு — await இல்லாமல் coroutine-ஐ அழைப்பது:

async def main():
    rate = fetch_rate("USD")    # await மறந்தது
    print(rate * 2)             # TypeError
# RuntimeWarning: coroutine 'fetch_rate' was never awaited

Python செயல்பாட்டை இயக்கவில்லை, coroutine பொருளை மட்டுமே உருவாக்கியது. பதிவுகளில் "was never awaited" தோன்றினால் விடுபட்ட await-ஐத் தேடுங்கள். இரண்டாவது பொதுவான பிழை — coroutine-க்குள் தடுக்கும் அழைப்பு (time.sleep, requests.get). HTTP-க்கு ஒத்திசைவற்ற நூலகங்களை (httpx, aiohttp) எடுங்கள்; தவிர்க்க முடியாத ஒத்திசைவான குறியீட்டை await asyncio.to_thread(func, arg) மூலம் ஓர் இழைக்கு அனுப்புங்கள்.

Python-இல் பல இழைகள்: threading தொகுதி

இழை (thread) — ஒரே செயல்முறைக்குள் சுயாதீனமான இயக்கக் கோடு; இழைகள் பொதுவான நினைவகத்தைப் பகிர்கின்றன. பிணைய காத்திருப்புக்கு அவை சிறப்பாக வேலை செய்கின்றன; குறியீட்டை async-ஆக மாற்றவும் தேவையில்லை.

import threading, time

def download(name):
    time.sleep(1)               # பிணைய காத்திருப்பு
    print(name, "தயார்")

threads = [threading.Thread(target=download, args=(n,)) for n in ("a", "b", "c")]
for t in threads:
    t.start()
for t in threads:
    t.join()                    # எல்லாம் சுமார் 1 வினாடியில்

பொதுவான நினைவகம் நன்மையும் ஆதாரப் பிழைகளின் மூலமுமாகும்: இரண்டு இழைகள் ஒரே மாறியை மாற்றினால் threading.Lock தேவை. பொதுவாக இழைகளை கைமுறையாகக் கையாளாமல், ஒரு குளத்தை (pool) எடுப்பது எளிது.

ThreadPoolExecutor

concurrent.futures.ThreadPoolExecutor இழைகளின் குளத்தை உருவாக்கி அவற்றுக்குப் பணிகளை வழங்குகிறது. map முடிவுகளை மூல வரிசையில் திருப்பும்.

from concurrent.futures import ThreadPoolExecutor
import time

def fetch(url):
    time.sleep(1)
    return f"{url}: 200"

urls = [f"https://example.com/{i}" for i in range(5)]
with ThreadPoolExecutor(max_workers=5) as pool:
    for result in pool.map(fetch, urls):
        print(result)           # எல்லாம் சுமார் 1 வினாடியில்

GIL என்றால் என்ன, இழைகள் ஏன் கணக்கீட்டை வேகப்படுத்துவதில்லை

GIL (Global Interpreter Lock) — CPython மொழிபெயர்ப்பியின் பொதுப் பூட்டு. எந்த நேரத்திலும் Python பைட்குறியீட்டை ஒரே ஒரு இழை மட்டுமே இயக்குகிறது. ஒரு இழை உள்ளீடு-வெளியீட்டுக்காகக் காத்திருக்கும்போது அது GIL-ஐ விடுவிக்கும்; அதனால் பிணையத்துக்கு இழைகள் பயனுள்ளவை. ஆனால் சுத்தக் கணக்கீட்டில் நான்கு இழைகளும் ஒரு இழையைவிட வேகமாக ஓடாது: அவை பூட்டை முறைக்கு ஒன்றாக கைமாற்றிக் கொள்கின்றன.

from concurrent.futures import ThreadPoolExecutor

def heavy(n):
    return sum(i * i for i in range(n))

with ThreadPoolExecutor(4) as pool:
    list(pool.map(heavy, [5_000_000] * 4))   # ஏறக்குறைய வரிசையாக ஓடுவது போலவே

C-இல் எழுதப்பட்ட நூலகங்கள் (NumPy போன்றவை) கனமான செயல்பாடுகளின்போது GIL-ஐ பெரும்பாலும் விடுவிக்கும்; அதனால் அவற்றுக்கு இழைகள் வேகம் தருகின்றன.

multiprocessing: உண்மையான இணைச் செயல்பாடு

ஒவ்வொரு செயல்முறைக்கும் தனி மொழிபெயர்ப்பி மற்றும் தனி GIL உண்டு; அதனால் செயல்முறைகள் வெவ்வேறு கோர்களில் ஒரே நேரத்தில் கணக்கிடுகின்றன. விலை — தனி நினைவகம்: செயல்முறைகளுக்கிடையே தரவு தொடராக்கம் (pickle) மூலம் அனுப்பப்படுகிறது, செயல்முறை தொடங்குவது இழையைவிடச் செலவானது.

from concurrent.futures import ProcessPoolExecutor

def heavy(n):
    return sum(i * i for i in range(n))

if __name__ == "__main__":      # Windows மற்றும் macOS-இல் கட்டாயம்
    with ProcessPoolExecutor() as pool:
        print(list(pool.map(heavy, [5_000_000] * 4)))

if __name__ == "__main__" சோதனை இல்லாவிட்டால் குழந்தை செயல்முறைகள் தொகுதியை இறக்குமதி செய்யும்போது புதிய செயல்முறைகளைத் தொடங்கத் தொடங்கும். Process, Pool, Queue போன்றவற்றுடன் கீழ்நிலை multiprocessing தொகுதியும் உண்டு; ஆனால் ProcessPoolExecutor எளிதானது, இழைக் குளத்தின் அதே இடைமுகத்தைக் கொண்டது.

எதைத் தேர்ந்தெடுப்பது: asyncio, threading அல்லது multiprocessing

பணிகருவிஏன்
200 பக்கங்களைப் பதிவிறக்குவது, பல API கோரிக்கைகள்asyncio (httpx/aiohttp) அல்லது ThreadPoolExecutorபிணைய காத்திருப்பு, செயலி சும்மா
ஏற்கனவே ஒத்திசைவற்ற நிரலுக்குள் பல கோரிக்கைகள்TaskGroupகுறியீடு ஏற்கனவே async
ஒத்திசைவற்ற பதிப்பு இல்லாத ஒத்திசைவான நூலகம்இழைகள், asyncio.to_threadகுறியீட்டை மீண்டும் எழுத வேண்டியதில்லை
சுத்த Python-இல் கனமான கணக்கீடுProcessPoolExecutorGIL-ஐத் தவிர்க்கும், எல்லாக் கோர்களும்

GIL இல்லாத Python மற்றும் துணை மொழிபெயர்ப்பிகள்: புதியவை

Python 3.13 முதல் GIL இல்லாத தனி உருவாக்கம் உண்டு (free-threading, PEP 703) — அதன் பெயரில் t எழுத்து இருக்கும், உதாரணமாக python3.15t. 3.13-இல் அது சோதனை நிலையில் இருந்தது; 3.14-இலிருந்து (PEP 779) அது அதிகாரப்பூர்வமாக ஆதரிக்கப்படுகிறது. அதில் இழைகள் பல கோர்களில் ஒரே நேரத்தில் கணக்கிடுகின்றன. ஆனால் இயல்பாக நீங்கள் இன்னும் GIL கொண்ட சாதாரண Python-ஐயே பெறுவீர்கள்: ஒரு இழை குறியீடு free-threaded உருவாக்கத்தில் சற்று மெதுவாக ஓடும், எல்லா நூலகங்களும் அதற்குத் தயாராக இல்லை.

import sys
print(sys._is_gil_enabled())   # free-threaded உருவாக்கத்தில் False

இணைக் கணக்கீட்டுக்கான இரண்டாவது வழி 3.14-இல் வந்தது (PEP 734) — concurrent.interpreters தொகுதியின் துணை மொழிபெயர்ப்பிகள்: ஒரே செயல்முறைக்குள் தனிமைப்படுத்தப்பட்ட பல மொழிபெயர்ப்பிகள், ஒவ்வொன்றுக்கும் சொந்த GIL. InterpreterPoolExecutor மூலம் அவற்றைப் பயன்படுத்துவது எளிது; அது இழைக் குளத்தைப் போலவே இருக்கும். பாடநெறியின் அளவீட்டில் நான்கு கனமான பணிகள் 0.39 வி எடுத்தன, ஒரே இழையில் 1.03 வி-க்கு எதிராக.

சுருக்கம்

Asyncio python காத்திருப்புப் பணிகளுக்கான கருவி: async def-இல் coroutine-கள், await-இல் இடைநிறுத்தங்கள், TaskGroup அல்லது gather மூலம் ஒரே நேரத்தில் தொடக்கம். குறியீட்டை async-ஆக மாற்றாமல் இழைகள் அதே வேலையைச் செய்யும்; கனமான கணக்கீடுகளுக்கு செயல்முறைகள் தேவை, ஏனெனில் சாதாரண CPython-இல் GIL இழைகளை ஒரே நேரத்தில் கணக்கிட விடுவதில்லை. Free-threading மற்றும் துணை மொழிபெயர்ப்பிகள் இந்தப் படத்தை மெல்லமாக மாற்றுகின்றன, ஆனால் அடிப்படை விதி — "காத்திருந்தால் asyncio அல்லது இழைகள்; கணக்கிட்டால் செயல்முறைகள்" — இன்னும் பொருந்துகிறது. பாடநெறியில் இவை அனைத்தும் நடைமுறையில் விளக்கப்பட்டுள்ளன: செலவு கண்காணிப்பான் நாணய விகிதங்களை ஒரே நேரத்தில் ஒரு வினாடியில் பெறுகிறது, மூன்று வினாடிகளுக்குப் பதிலாக.

அடிக்கடி கேட்கப்படும் கேள்விகள்

ஒரு சாதாரண குறியீட்டிலிருந்து async செயல்பாட்டை அழைக்க முடியுமா?

ஆம், <code>asyncio.run(coro())</code> மூலம்: அது நிகழ்வுச் சுழற்சியை உருவாக்கி, coroutine-ஐ இயக்கி, சுழற்சியை மூடும். ஏற்கனவே இயங்கும் நிகழ்வுச் சுழற்சிக்குள் <code>asyncio.run</code>-ஐ அழைக்க முடியாது; அங்கே <code>await</code> பயன்படுத்துங்கள்.

Asyncio பல இழைகளில் வேலை செய்கிறதா?

இல்லை, நிகழ்வுச் சுழற்சி எல்லா coroutine-களையும் ஒரே இழையில் இயக்குகிறது; <code>await</code> புள்ளிகளில் மட்டுமே மாறுகிறது. அதனால் இழைகளை விட தரவு இனங்கள் குறைவு, ஆனால் ஒரு தடுக்கும் செயல்பாடு எல்லாவற்றையும் நிறுத்தும்.

asyncio-ஐ pip மூலம் நிறுவ வேண்டுமா?

இல்லை, asyncio Python 3-இன் நிலையான நூலகத்தில் உள்ளது; <code>import asyncio</code> எழுதினால் போதும். PyPI-இல் உள்ள asyncio தொகுப்பு காலாவதியானது, அதை நிறுவ வேண்டியதில்லை.

தடுக்காமல் கனமான செயல்பாட்டை asyncio-இலிருந்து எப்படி இயக்குவது?

கணக்கீடுகளுக்கு ProcessPoolExecutor உடன் <code>loop.run_in_executor</code>, தடுக்கும் உள்ளீடு-வெளியீட்டுக்கு <code>asyncio.to_thread</code>; நிகழ்வுச் சுழற்சி மற்ற பணிகளைத் தொடர்ந்து கவனிக்கும்.

coroutine-க்கும் Task-க்கும் என்ன வேறுபாடு?

Coroutine என்பது <code>await</code> மூலம் காத்திருக்கப்படும் வரை எதுவும் செய்யாத பொருள். Task coroutine-ஐச் சுற்றி, உடனே நிகழ்வுச் சுழற்சியின் அட்டவணையில் வைக்கிறது; அதனால் அது மற்ற குறியீட்டுடன் இணையாக இயங்கும்.

இந்தத் தலைப்பை வீடியோவில் கற்றுக்கொள்ளுங்கள்

“Python 3.15: от новичка до профи” பாடநெறியில் இந்தத் தலைப்பு பயிற்சியுடன் விரிவாக விளக்கப்பட்டுள்ளது.

பாடநெறிக்குச் செல்