# Python में asyncio: async/await, TaskGroup, थ्रेड, multiprocessing और GIL

URL: https://skillaria.top/hi/guides/python/asyncio-python

Asyncio Python की बिल्ट-इन लाइब्रेरी है: async def वाले फ़ंक्शन (कोरूटीन) await पर रुकते हैं, और उस दौरान इवेंट लूप दूसरे काम करता है, इसलिए सैकड़ों नेटवर्क अनुरोध एक ही थ्रेड में एक साथ चलते हैं। नेटवर्क और डिस्क की प्रतीक्षा के लिए 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 क्या है: कोरूटीन और इवेंट लूप

असिंक्रोनस कोड दो शब्दों पर टिका है। `async def` एक कोरूटीन घोषित करता है — ऐसा फ़ंक्शन जो रुक सकता है। उसे बुलाने पर बॉडी नहीं चलती, सिर्फ़ कोरूटीन ऑब्जेक्ट बनता है। `await` भीतर कहता है: «यहाँ मैं इंतज़ार करूँगा, तब तक तुम दूसरे काम देखो»। पूरे सिलसिले को इवेंट लूप चलाता है, जिसे `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()` एक असिंक्रोनस विराम है: वह नियंत्रण इवेंट लूप को लौटा देता है। कोरूटीन के भीतर साधारण `time.sleep()` पूरे लूप को रोक देगा, और बाक़ी सारे कार्य खड़े रहेंगे।

### एक के बाद एक या एक साथ

लूप में `await` लिखें, तो अनुरोध एक-एक करके चलेंगे — तीन मुद्राओं में तीन सेकंड लगेंगे। एक साथ चलाने के लिए उन्हें कार्य (task) के रूप में शुरू करना होगा।

```
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()` था: वह कोरूटीन एक साथ चलाता है और परिणाम उसी क्रम में लौटाता है। पुराने कोड में यह आज भी बहुत मिलता है।

```
async def main():
    usd, eur, cny = await asyncio.gather(
        fetch_rate("USD"), fetch_rate("EUR"), fetch_rate("CNY")
    )
    # return_exceptions=True के साथ एरर सूची में लौटेंगी, सब कुछ नहीं रोकेंगी
```

`asyncio.create_task()` बिना ग्रुप के कोरूटीन को पृष्ठभूमि में चलाता है। टास्क का संदर्भ सहेजना ज़रूरी है, वरना गार्बेज कलेक्टर उसे हटा सकता है और एरर किसी को दिखेगी नहीं। इसलिए नए प्रोजेक्ट में 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` — क्रिटिकल सेक्शन में एक समय में सिर्फ़ एक कोरूटीन।
- `asyncio.Event` — एक कोरूटीन `await event.wait()` पर रुकती है, दूसरी `event.set()` से संकेत देती है।
- `asyncio.Queue` — «उत्पादक — उपभोक्ता» की कतार: `await q.put()`, `await q.get()`, `q.task_done()`।

## जाल: await भूल जाना

सबसे आम गलती: कोरूटीन को बिना `await` के बुलाना।

```
async def main():
    rate = fetch_rate("USD")    # await भूल गए
    print(rate * 2)             # TypeError
# RuntimeWarning: coroutine 'fetch_rate' was never awaited
```

Python ने फ़ंक्शन नहीं चलाया, सिर्फ़ कोरूटीन ऑब्जेक्ट बनाया। लॉग में «was never awaited» दिखे, तो छूटा हुआ `await` खोजें। दूसरी आम गलती है कोरूटीन के भीतर ब्लॉक करने वाला कॉल (`time.sleep`, `requests.get`)। HTTP के लिए असिंक्रोनस लाइब्रेरी लें (httpx, aiohttp), और जो सिंक्रोनस कोड बदला न जा सके, उसे `await asyncio.to_thread(func, arg)` से थ्रेड में भेजें।

## Python में मल्टीथ्रेडिंग: threading मॉड्यूल

थ्रेड एक ही प्रोसेस के भीतर स्वतंत्र निष्पादन-धारा है, और थ्रेड मेमोरी साझा करते हैं। नेटवर्क की प्रतीक्षा के लिए वे बहुत अच्छे हैं, और कोड को 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` चाहिए। आम तौर पर थ्रेड हाथ से न बनाएँ, पूल लें।

### 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__"` के बिना बच्चे प्रोसेस मॉड्यूल इम्पोर्ट होते समय नए प्रोसेस बनाने लगेंगे। एक निचले स्तर का मॉड्यूल `multiprocessing` भी है, जिसमें `Process`, `Pool`, `Queue` हैं, पर `ProcessPoolExecutor` सरल है और उसका इंटरफ़ेस थ्रेड-पूल जैसा है।

## क्या चुनें: asyncio, threading या multiprocessing

| काम | औज़ार | क्यों |
| --- | --- | --- |

| 200 पेज डाउनलोड करना, API पर बहुत अनुरोध | asyncio (httpx/aiohttp) या ThreadPoolExecutor | नेटवर्क की प्रतीक्षा, प्रोसेसर खाली |

| पहले से असिंक्रोनस प्रोग्राम में कई अनुरोध | TaskGroup | कोड पहले से async है |

| बिना async संस्करण वाली सिंक्रोनस लाइब्रेरी | थ्रेड, `asyncio.to_thread` | कोड दोबारा लिखना नहीं पड़ता |

| शुद्ध Python पर भारी गणना | ProcessPoolExecutor | GIL को पार करता है, सारे कोर |

## GIL के बिना Python और sub-interpreters: नया क्या है

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
```

समानांतर गणना का दूसरा रास्ता Python 3.14 (PEP 734) में आया — मॉड्यूल `concurrent.interpreters` में sub-interpreters: एक ही प्रोसेस में कई अलग इंटरप्रेटर, और हर का अपना GIL। इन्हें सबसे आसानी से `InterpreterPoolExecutor` से चलाया जाता है, जिसका रूप थ्रेड-पूल जैसा है। कोर्स के माप में चार भारी कार्य 0.39 सेकंड में पूरे हुए, जबकि एक थ्रेड में 1.03 सेकंड लगे।

## निष्कर्ष

Python का asyncio इंतज़ार वाले कामों के लिए है: `async def` की कोरूटीन, `await` पर विराम, और TaskGroup या `gather` से एक साथ चलाना। थ्रेड उसी काम को बिना कोड async किए कर देते हैं, और भारी गणना के लिए प्रोसेस चाहिए, क्योंकि GIL साधारण CPython के थ्रेड्स को समानांतर गणना नहीं करने देता। Free-threading और sub-interpreters इस तस्वीर को धीरे-धीरे बदल रहे हैं, पर मूल नियम «इंतज़ार — asyncio या थ्रेड, गणना — प्रोसेस» अब भी काम करता है। कोर्स में यह विषय व्यवहार में सिखाया जाता है: ख़र्च ट्रैकर मुद्रा दरें एक साथ लेता है और तीन सेकंड की जगह एक सेकंड में तैयार हो जाता है।

## FAQ

**क्या किसी सिंक्रोनस फ़ंक्शन से async फ़ंक्शन बुलाया जा सकता है?**

हाँ, asyncio.run(coro()) से: वह इवेंट लूप बनाता है, कोरूटीन चलाता है और लूप बंद करता है। जो लूप पहले से चल रहा हो, उसके भीतर asyncio.run नहीं चलता — वहाँ await लिखा जाता है।

**क्या asyncio कई थ्रेड में चलता है?**

नहीं, इवेंट लूप सारी कोरूटीन एक ही थ्रेड में चलाता है और सिर्फ़ await बिंदुओं पर बदलता है। इसलिए डेटा-रेस थ्रेड से कम होती हैं, पर एक ब्लॉक करने वाला कॉल सब कुछ रोक देता है।

**क्या asyncio को pip से इंस्टॉल करना पड़ता है?**

नहीं, asyncio Python 3 की स्टैंडर्ड लाइब्रेरी में है; import asyncio लिखना काफ़ी है। PyPI पर asyncio नाम का पैकेज पुराना है, उसे इंस्टॉल करने की ज़रूरत नहीं।

**asyncio से भारी फ़ंक्शन चलाएँ, बिना लूप रोके, तो कैसे?**

गणना के लिए loop.run_in_executor के साथ ProcessPoolExecutor, और ब्लॉक करने वाले I/O के लिए asyncio.to_thread। इवेंट लूप बाक़ी टास्क चलाता रहेगा।

**कोरूटीन और टास्क (Task) में क्या फ़र्क है?**

कोरूटीन एक ऑब्जेक्ट है, जो तब तक कुछ नहीं करता जब तक उसका await न हो। टास्क कोरूटीन को लपेटता है और उसे तुरंत इवेंट लूप के शेड्यूल में डाल देता है, इसलिए वह बाक़ी कोड के साथ समानांतर चलता है।


---
Source: Skillaria.Top (https://skillaria.top)
