সরাসরি পর্বে যান
পর্ব ৪২ · ৫৫-এর মধ্যে · মডিউল ৬

asyncio — এক thread-এ অনেক অপেক্ষা, async আর await

asyncio — many waits on one thread with async & await

Taught in Bangla, this lesson explains asyncio as one waiter serving many tables: async def, coroutine objects and await, the event loop, running tasks together with gather, create_task and TaskGroup, the blocking-call bug and asyncio.to_thread, timeouts and cancellation, Queue and Semaphore, async with and async for, and when to choose threads or processes instead.

এই পর্বে যা শিখবেন

  • event loop কীভাবে await-এর জায়গায় কাজ বদলায়, তা ধাপে ধাপে ছবি এঁকে বোঝা
  • coroutine object, “never awaited” সতর্কবার্তা আর asyncio.run-এর ভূমিকা
  • gather, create_task, TaskGroup দিয়ে একসাথে চালানো, আর except* দিয়ে একাধিক ভুল ধরা
  • timeout, cancellation, Queue, Semaphore, to_thread, async with/async for — বাস্তব কাজে
  • কখন asyncio, কখন thread, কখন process — আর AI-এর লেখা async কোডের চেনা ভুল ধরা

১ · এক ওয়েটার, অনেক টেবিল — asyncio-র মূল ভাবনা

পুরান ঢাকার একটি জমজমাট রেস্টুরেন্টের কথা ভাবুন। দুপুরে দশটি টেবিলে লোক বসে আছে, ওয়েটার একজন। তিনি যদি প্রথম টেবিলে গিয়ে দাঁড়িয়ে থাকেন যতক্ষণ না সেই পরিবার মেনু দেখে সিদ্ধান্ত নেয়, তারপর রান্নাঘরের দরজায় দাঁড়িয়ে থাকেন যতক্ষণ না খাবার তৈরি হয় — তাহলে বাকি নয়টি টেবিল আধা ঘণ্টা বসে থাকবে। অথচ ওয়েটারের নিজের কাজ (অর্ডার লেখা, প্লেট নিয়ে আসা) খুবই অল্প সময়ের। বেশিরভাগ সময় যায় অপেক্ষায়।

ভালো ওয়েটার তাই অপেক্ষা করেন না। প্রথম টেবিল “একটু ভেবে বলছি” বললে তিনি দ্বিতীয় টেবিলের অর্ডার নেন। রান্নাঘরে অর্ডার দিয়ে তৃতীয় টেবিলে পানি দেন। রান্নাঘর থেকে “খিচুড়ি রেডি” ডাক এলে প্লেট নিয়ে যান। মানুষ একজনই, এক মুহূর্তে একটিই কাজ — কিন্তু অপেক্ষার ফাঁকগুলো কাজে লাগানোয় দশটি টেবিলই প্রায় একসাথে খাবার পায়। asyncio ঠিক এই ওয়েটার। ওয়েটার = একটি thread; টেবিল = একেকটি কাজ; “একটু ভেবে বলছি” = await; আর কোন টেবিল এখন প্রস্তুত, সেই হিসাব রাখা খাতা = event loop.

প্রোগ্রামেও এমন অপেক্ষা সবখানে। একটি API-তে অনুরোধ পাঠালে উত্তর আসতে ১০০–৫০০ মিলিসেকেন্ড লাগতে পারে; ডেটাবেস থেকে উত্তর, ফাইল ডাউনলোড, ব্যবহারকারীর মেসেজের অপেক্ষা — এসব সময়ে CPU কার্যত বসে থাকে। এমন কাজকে বলে I/O-bound. উল্টো দিকে ছবি প্রসেস করা বা বড় হিসাব — যেখানে CPU সারাক্ষণ ব্যস্ত — সেটা CPU-bound. asyncio প্রথম ধরনের জন্য।

concurrency বনাম parallelism

এই দুটি শব্দ প্রায়ই গুলিয়ে যায়। concurrency মানে অনেক কাজ একই সময়সীমার মধ্যে এগিয়ে চলছে — একজন ওয়েটার দশ টেবিল সামলাচ্ছেন। parallelism মানে অনেক কাজ একই মুহূর্তে সত্যিই চলছে — চারজন রাঁধুনি চারটি চুলায় একসাথে রাঁধছেন। asyncio দেয় concurrency, parallelism নয়: এক thread, এক মুহূর্তে একটিই Python লাইন চলে। একাধিক CPU core সত্যিই একসাথে কাজে লাগাতে হলে process বা (free-threaded Python-এ) thread লাগে — পর্ব ৪১-এ দেখেছেন।

মূল ধারণা

asyncio-তে কাজ বদল হয় শুধু await-এর জায়গায়, আর সেটা কাজটি নিজে ঠিক করে — “আমি এখন অপেক্ষা করব, অন্য কেউ চলুক”. একে বলে cooperative multitasking. thread-এ অপারেটিং সিস্টেম যেকোনো মুহূর্তে জোর করে কাজ বদলাতে পারে (preemptive); asyncio-তে কেউ জোর করে না। সুবিধা: দুটি await-এর মাঝের কোড কেউ মাঝপথে থামাবে না, তাই race condition অনেক কম। বিপদ: যে কাজ কখনো await করে না, সে সবাইকে আটকে রাখে — রেস্টুরেন্টে এক টেবিলে গল্পে মজে যাওয়া ওয়েটারের মতো।

২ · async def, coroutine object আর await

পর্ব ৩৪-এ দেখেছেন, yield থাকা ফাংশন ডাকলে body চলে না — পাওয়া যায় একটি generator, যা থেমে থেমে চলে। asyncio-র ভিত্তিও সেই “থামো, অবস্থা রাখো, পরে চলো” যন্ত্র, শুধু নিজস্ব syntax-এ:

  • async def দিয়ে লেখা ফাংশনকে বলে coroutine function. একে ডাকলে body-র একটি লাইনও চলে না; ফেরত আসে একটি coroutine object — চালানোর অপেক্ষায় থাকা একটি কাজ।
  • await x লেখা যায় শুধু async def-এর ভেতরে। এর মানে: “x শেষ হওয়া পর্যন্ত আমি থামলাম; এই ফাঁকে event loop অন্য কাজ চালাক। x-এর ফল এলে আমাকে এখান থেকেই আবার চালাও।” যার ওপর await করা যায়, তাকে বলে awaitable — coroutine, Task আর Future.
  • asyncio.run(main()) সাধারণ (sync) জগৎ থেকে async জগতে ঢোকার দরজা: একটি নতুন event loop বানায়, main() শেষ হওয়া পর্যন্ত চালায়, তারপর বাকি কাজ পরিষ্কার করে loop বন্ধ করে। একটি প্রোগ্রামে সাধারণত একবারই ডাকা হয় (Python 3.7 থেকে আছে)।
import asyncio

async def greet(name):
    await asyncio.sleep(1)      # ১ সেকেন্ড অপেক্ষা — loop তখন মুক্ত
    return f"আসসালামু আলাইকুম, {name}"

async def main():
    msg = await greet("রাফি")   # চালাও, শেষ হলে ফল নাও
    print(msg)

asyncio.run(main())             # sync জগৎ থেকে async জগতে

await ভুলে গেলে — “never awaited”

নতুনদের (আর AI-এরও) সবচেয়ে প্রচলিত ভুল: coroutine ডেকে await না লেখা। send_sms("রহিম") লিখলে মনে হয় SMS চলে গেল, কিন্তু আসলে শুধু একটি coroutine object তৈরি হয়ে পড়ে থাকল — কেউ তাকে চালায়নি। কোনো exception আসে না; প্রোগ্রাম আনন্দে এগিয়ে যায়। পরে সেই object মুছে ফেলার সময় Python stderr-এ একটি সতর্কবার্তা লেখে:

main.py:8: RuntimeWarning: coroutine 'send_sms' was never awaited
  send_sms("রহিম")
RuntimeWarning: Enable tracemalloc to get the object allocation traceback

এটা error নয়, RuntimeWarning — তাই স্ক্রিপ্ট থামে না, আর লগের ভিড়ে সহজেই চোখ এড়িয়ে যায়। বার্তাটির মানে: “আপনি একটি কাজ বানিয়েছিলেন, কিন্তু কখনো চালাননি।” দ্বিতীয় লাইনটি শুধু বলে, চাইলে tracemalloc চালু করে জানা যাবে object-টি কোথায় তৈরি হয়েছিল। সমাধান প্রায় সবসময় এক শব্দ: await send_sms("রহিম"). একই কারণে result = fetch() লিখে result ছাপালে দেখবেন <coroutine object fetch at 0x...>, আসল ফল নয়।

সাধারণ ফাংশন থেকে await করা যায় না। def-এর ভেতরে await লিখলে SyntaxError: 'await' outside async function. আর async ফাংশন থেকে ফল পেতে সাধারণ কোডকে asyncio.run ডাকতে হয় — কিন্তু এমন জায়গায়, যেখানে আগে থেকে কোনো loop চলছে না (চলমান loop-এর ভেতর থেকে ডাকলে RuntimeError). এই “দুই রঙের ফাংশন” সমস্যা নিয়ে ভাবনার প্রশ্ন ৩.

৩ · event loop — ওয়েটারের খাতা

event loop একটি সাধারণ লুপ, যা মোটামুটি এই কাজ বারবার করে:

  1. যেসব কাজ এখন চলার জন্য প্রস্তুত (ready), সেগুলোকে একটি একটি করে চালাও — প্রতিটি চলবে তার পরের await পর্যন্ত। (Task আসলে একটি callback-এর মতো loop-এর তালিকায় থাকে।)
  2. যে কাজ await-এ থামল, সে কিসের অপেক্ষায় — একটি টাইমার, না একটি নেটওয়ার্ক socket — খাতায় লিখে রাখো।
  3. কেউ প্রস্তুত না থাকলে অপারেটিং সিস্টেমকে বলো: “এই socket-গুলোর কোনোটিতে ডেটা এলে, বা সবচেয়ে কাছের টাইমার বাজলে আমাকে জাগিও।” (Linux-এ এটা epoll, macOS-এ kqueue — একে বলে non-blocking I/O.)
  4. জেগে উঠে যাদের অপেক্ষা শেষ, তাদের আবার ready তালিকায় তোলো। ১ নম্বরে ফিরে যাও।

নিচে তিনটি কাজ — A, B, C — যাদের অপেক্ষা যথাক্রমে ৩, ১ আর ২ ইউনিট (ধরুন একেকটি ইউনিট এক সেকেন্ড)। ধাপে ধাপে দেখুন কে চলে, কে ঘুমায়, আর কে কখন জাগে:

1async def job(name, t):2    print(name, "শুরু")3    await asyncio.sleep(t)4    print(name, "শেষ")5async def main():6    await asyncio.gather(job("A", 3),7        job("B", 1), job("C", 2))
event loop-এর খাতা
ঘড়ি (ইউনিট)
এখন চলছে
ঘুমিয়ে (টাইমার)
শেষ হয়েছে
  1. main তিনটি কাজ জমা দিল। gather তিনটি coroutine-কে Task-এ মুড়ে loop-এর ready তালিকায় রাখল — A, B, C, এই ক্রমে। তারপর main নিজে await-এ থামল: “তিনজনই শেষ হলে আমাকে জাগিও।” এখনো কোনো job-এর একটি লাইনও চলেনি।
  2. A চলল — প্রথম await পর্যন্ত। loop তালিকার প্রথম কাজ A-কে চালাল। “A শুরু” ছাপা হলো, তারপর await asyncio.sleep(3). এখানে A বলল, “৩ ইউনিট পর আমাকে জাগিও” — একটি টাইমার খাতায় উঠল, আর নিয়ন্ত্রণ ফিরে গেল loop-এর কাছে। A ঘুমাচ্ছে, কিন্তু thread মুক্ত।
  3. B আর C — একই ঘটনা। loop B চালাল (টাইমার ১-এ), তারপর C (টাইমার ২-এ)। তিনটি “শুরু” প্রায় একই মুহূর্তে ছাপা হয়েছে, কারণ প্রত্যেকে মাত্র কয়েক মাইক্রোসেকেন্ড চলে await-এ থেমেছে। লক্ষ করুন: কোনো কাজকে জোর করে থামানো হয়নি — প্রত্যেকে নিজে await-এ সরে দাঁড়িয়েছে।
  4. কেউ প্রস্তুত নেই — loop ঘুমায়। ready তালিকা খালি। loop সবচেয়ে কাছের টাইমার দেখল (B, ১ ইউনিটে) আর অপারেটিং সিস্টেমকে বলল, “ততক্ষণ আমাকে ঘুম পাড়িয়ে রাখো, তবে কোনো নেটওয়ার্ক ডেটা এলে আগেই জাগিও।” এই সময়ে CPU প্রায় শূন্য খরচ করে — busy waiting নেই।
  5. ১ ইউনিট — B জাগল। B-এর টাইমার বাজল, B আবার ready. loop তাকে চালাল ঠিক await-এর পরের লাইন থেকে: “B শেষ”, আর B-এর Task শেষ। generator যেমন yield-এর পর থেকে চলে (পর্ব ৩৪), coroutine-ও await-এর পর থেকে।
  6. ২ ইউনিট — C জাগল। একই ঘটনা C-এর জন্য। এখন বাকি শুধু A.
  7. ৩ ইউনিট — A জাগল, gather শেষ। A শেষ হতেই gather দেখল তিনজনই শেষ, তাই main জাগল। মোট সময় ≈ ৩ ইউনিট — সবচেয়ে লম্বা অপেক্ষার সমান, তিনটির যোগফল ৬ নয়। শেষ হওয়ার ক্রম B, C, A হলেও gather ফলগুলো দেয় জমা দেওয়ার ক্রমে — A, B, C.

এই ছবিতে asyncio-র পুরো রহস্য আছে। কোনো জাদু নেই, কোনো লুকানো thread নেই — একটি লুপ, একটি ready তালিকা, কিছু টাইমার আর অপারেটিং সিস্টেমের কাছে “জাগিও” অনুরোধ। কাজ বদলের খরচ খুব কম (একটি ফাংশন আবার চালানোর মতো), তাই একটি loop হাজার হাজার সংযোগ সামলাতে পারে, যেখানে প্রতিটি সংযোগের জন্য একটি করে thread খুললে মেমরি আর বদলের খরচ অনেক বেশি পড়ে।

৪ · একসাথে চালানো — gather, create_task আর TaskGroup

একটি বাস্তব কাজ ধরুন: একটি রেমিট্যান্স অ্যাপ তিনটি ব্যাংকের API থেকে ডলারের রেট এনে সবচেয়ে ভালোটি দেখাবে। প্রতিটি API উত্তর দিতে কিছুটা সময় নেয়। শুধু await পরপর লিখলে কী হয়, আর একসাথে চালালে কী হয় — আগে ধাপে ধাপে দেখুন:

1# পরপর (sequential)2a = await fetch("A", 3)3b = await fetch("B", 1)4c = await fetch("C", 2)5# একসাথে (concurrent)6a, b, c = await asyncio.gather(7    fetch("A", 3), fetch("B", 1), fetch("C", 2))
সময়ের হিসাব (ধারণাগত ইউনিট, মাপা নয়)
ঘড়ি
কে অপেক্ষায়
মোট সময়
  1. প্রথম await — A-এর অপেক্ষা। await fetch("A", 3) মানে “A শেষ না হওয়া পর্যন্ত আমি পরের লাইনে যাব না।” loop মুক্ত ঠিকই, কিন্তু loop-এর কাছে চালানোর মতো আর কোনো কাজ নেই — B আর C তো এখনো তৈরিই হয়নি। ৩ ইউনিট নষ্ট।
  2. তারপর B. A-এর উত্তর আসার পর তবেই B-এর অনুরোধ যায়। async def আর await লেখা সত্ত্বেও এখানে কোনো concurrency নেই — কোড দেখতে async, আচরণে sync.
  3. তারপর C — মোট ৬। তিনটি অপেক্ষা পরপর জুড়ে গেছে: ৩ + ১ + ২ = ৬. এটা AI-এর লেখা কোডে খুব দেখা যায় — লুপের ভেতরে await, প্রতিটি অনুরোধ আগেরটির শেষের অপেক্ষায়।
  4. gather — তিনটিই একসাথে শুরু। gather তিনটি coroutine-কে Task বানিয়ে একসাথে loop-এ দেয়। শূন্য মুহূর্তেই তিনটি অনুরোধ চলে গেছে; এখন তিনজনই একসাথে অপেক্ষা করছে।
  5. মোট ৩ — সবচেয়ে লম্বাটির সমান। B ১-এ, C ২-এ, A ৩-এ শেষ। অপেক্ষাগুলো একটির ওপর আরেকটি পড়েছে (overlap). ১০০টি অনুরোধ হলে পার্থক্য আরও নাটকীয় — তবে সার্ভারের ওপর একসাথে ১০০টি চাপ দেওয়া ভদ্রতা নয়; সীমা টানার উপায় অংশ ৭-এ।

এবার আসল কোড। নেটওয়ার্কের বদলে asyncio.sleep দিয়ে অপেক্ষার ভান করা হয়েছে (০.০১–০.০৩ সেকেন্ড), যাতে আউটপুট প্রতিবার একই আসে। প্রথমে দেখানো হয়েছে যে coroutine ডাকলে শুধু object তৈরি হয়:

import asyncio

async def fetch_rate(source, delay, rate):
    print(f"{source}: অনুরোধ গেল")
    await asyncio.sleep(delay)          # নেটওয়ার্কের অপেক্ষা (নকল)
    print(f"{source}: উত্তর এল")
    return rate

async def main():
    coro = fetch_rate("পরীক্ষা", 0, 0)
    print(type(coro).__name__)          # এখনো কিছুই চলেনি
    coro.close()                        # না চালিয়ে বন্ধ করা
    rates = await asyncio.gather(
        fetch_rate("ব্যাংক-A", 0.03, 121.50),
        fetch_rate("ব্যাংক-B", 0.01, 121.75),
        fetch_rate("ব্যাংক-C", 0.02, 121.60),
    )
    print("রেট:", rates, "সেরা:", max(rates))

asyncio.run(main())

প্রথম লাইন coroutine — fetch_rate(...) ডাকা হয়েছে, কিন্তু “অনুরোধ গেল” ছাপা হয়নি; body চলেনি। coro.close() দিয়ে আমরা জানিয়ে দিলাম যে এটা ইচ্ছে করেই চালানো হবে না, তাই “never awaited” সতর্কবার্তা আসে না। তারপর তিনটি “অনুরোধ গেল” — A, B, C ক্রমে, কারণ gather এই ক্রমেই Task জমা দেয় আর প্রত্যেকে প্রথম await পর্যন্ত চলে থামে। এরপর “উত্তর এল” আসে B, C, A ক্রমে — যার অপেক্ষা যত ছোট, সে তত আগে। শেষ লাইন রেট: [121.5, 121.75, 121.6] সেরা: 121.75 — শেষ হওয়ার ক্রম যা-ই হোক, gather ফলের list দেয় argument-এর ক্রমে। পুরো প্রোগ্রামে সময় লাগে প্রায় ০.০৩ সেকেন্ড (সবচেয়ে লম্বাটি), ০.০৬ নয়।

create_task — কাজ শুরু করে দিয়ে এগিয়ে যাওয়া

asyncio.create_task(coro) একটি coroutine-কে সাথে সাথে Task হিসেবে loop-এ জমা দেয় আর একটি Task object ফেরত দেয়। আপনি অন্য কাজ করতে পারেন, পরে await task লিখে ফল নিতে পারেন। একে বলা যায় “অর্ডার দিয়ে রাখা”:

async def main():
    t1 = asyncio.create_task(fetch_rate("ব্যাংক-A", 0.03, 121.50))
    t2 = asyncio.create_task(fetch_rate("ব্যাংক-B", 0.01, 121.75))
    print("দুটি অনুরোধ পাঠানো হলো, এবার অন্য কাজ...")
    a = await t1          # দুটিই ইতিমধ্যে চলছে
    b = await t2
    print(max(a, b))

Task-এর reference রাখুন। asyncio.create_task(send_report()) লিখে ফল কোথাও না রাখলে (“fire and forget”) একটি সূক্ষ্ম বিপদ আছে: event loop Task-গুলোকে শুধু weak reference দিয়ে মনে রাখে। আর কোথাও reference না থাকলে garbage collector কাজ শেষ হওয়ার আগেই Task মুছে ফেলতে পারে। Python-এর অফিসিয়াল ডকুমেন্টেশন নিজেই সতর্ক করে: ফলটি একটি variable বা set-এ রাখুন। আরও ভালো — TaskGroup ব্যবহার করুন, যে নিজেই সব Task-এর হিসাব রাখে।

TaskGroup — গুছানো concurrency (Python 3.11 থেকে)

gather আর create_task-এর একটি দুর্বলতা আছে: কোনো একটি কাজ ব্যর্থ হলে বাকিরা কী করবে? gather-এ প্রথম exception আপনার কাছে চলে আসে, কিন্তু অন্য Task-গুলো পেছনে চলতেই থাকে — কেউ তাদের থামায় না, ফলও কেউ নেয় না। Python 3.11 এর সমাধান হিসেবে আনে asyncio.TaskGroup, যা async with দিয়ে লেখা হয়:

async def main():
    async with asyncio.TaskGroup() as tg:
        ta = tg.create_task(fetch_rate("ব্যাংক-A", 0.03, 121.50))
        tb = tg.create_task(fetch_rate("ব্যাংক-B", 0.01, 121.75))
    # এখানে পৌঁছানো মানে: ভেতরের সব Task শেষ
    print(ta.result(), tb.result())

নিয়ম তিনটি, আর তিনটিই নিরাপত্তার জন্য। এক — async with ব্লক থেকে বের হওয়া যায় শুধু তখন, যখন ভেতরে শুরু হওয়া সব Task শেষ; কোনো কাজ পেছনে “হারিয়ে” যেতে পারে না। দুই — কোনো একটি Task exception দিলে বাকি সবাইকে cancel করা হয়। তিন — সব ভুল একসাথে একটি ExceptionGroup-এ মুড়ে ওপরে পাঠানো হয়, যা পর্ব ২৪-এ দেখা except* দিয়ে ধরন অনুযায়ী আলাদা করে ধরা যায়। এই ভাবনার নাম structured concurrency: কাজের আয়ু কোডের একটি ব্লকের সাথে বাঁধা, ঠিক যেমন with open(...) ফাইলের আয়ু ব্লকের সাথে বাঁধে। নতুন কোডে TaskGroup-ই প্রথম পছন্দ।

1try:2    async with asyncio.TaskGroup() as tg:3        tg.create_task(sms("রহিম", 3))4        tg.create_task(sms("ভুল-নম্বর", 1))5        tg.create_task(sms("করিম", 2))6except* ValueError as eg:7    print("ব্যর্থ:", eg.exceptions)
তিনটি Task-এর অবস্থা
রহিম (৩ ইউনিট)
ভুল-নম্বর (১)
করিম (২)
TaskGroup
  1. তিনটি SMS একসাথে শুরু। TaskGroup-এর ভেতরে তিনটি Task তৈরি হলো। async with ব্লকের শেষে এসে TaskGroup অপেক্ষা করছে — তিনটি শেষ না হলে সে কাউকে বের হতে দেবে না।
  2. ১ ইউনিটে একটি ব্যর্থ। “ভুল-নম্বর” Task-টি ValueError দিল। gather হলে এখানে exception আপনার কাছে চলে আসত, আর রহিম আর করিমের SMS পেছনে চলতেই থাকত — কেউ খেয়াল রাখত না।
  3. বাকিদের cancel. TaskGroup বাকি দুটি Task-কে cancel করল: প্রত্যেকে যে await-এ থেমে আছে, ঠিক সেখানে একটি CancelledError ঢোকানো হলো। তাদের ভেতরে finally বা async with থাকলে পরিষ্কারের কাজ এখন চলবে।
  4. সবাই থামলে তবেই বের হওয়া। cancel হওয়া Task-গুলো সত্যিই শেষ হওয়া পর্যন্ত TaskGroup অপেক্ষা করল। তারপর ব্লক থেকে উঠল একটি ExceptionGroup, যার ভেতরে আসল ValueError — একাধিক Task একসাথে ব্যর্থ হলে সবগুলোই এখানে থাকত।
  5. except* দিয়ে ধরা। except* ValueError গ্রুপের ভেতর থেকে ValueError-গুলো বেছে নিল। অন্য ধরনের ভুল থাকলে সেগুলো ওপরে চলে যেত, বা আরেকটি except* ধরত। কোনো কাজ পেছনে হারায়নি, কোনো ভুল চাপা পড়েনি — এটাই structured concurrency-র প্রতিশ্রুতি।
টুলকখনএকটি ব্যর্থ হলে
await coroএকটি কাজ, তার ফল লাগবেই পরের লাইনেexception সরাসরি আপনার কাছে
gather(*coros)কয়েকটি কাজ, ফল ক্রমমতো list-এপ্রথম exception আসে, বাকিরা চলতে থাকে; return_exceptions=True দিলে ভুলগুলোও list-এ আসে
create_task(coro)এখন শুরু, ফল পরেআপনি await না করলে ভুল হারিয়ে যেতে পারে
TaskGroup (3.11+)নতুন কোডে কয়েকটি কাজের জন্য প্রথম পছন্দবাকিদের cancel, সব ভুল ExceptionGroup-এ

৫ · সবচেয়ে বড় বাগ — event loop আটকে দেওয়া

অংশ ১-এর মূল ধারণা মনে করুন: কাজ বদল হয় শুধু await-এ। তাহলে কী হবে, যদি একটি coroutine এমন কিছু করে যা অপেক্ষা করে, কিন্তু await দিয়ে নয়? time.sleep(3) হলো এমন একটি blocking call: সে পুরো thread-কে ৩ সেকেন্ড ঘুম পাড়িয়ে দেয়। asyncio-তে thread একটিই — তাই loop-ও ঘুমায়, আর সাথে বাকি সব কাজ। ধাপে ধাপে দেখুন:

1async def ticker():          # প্রতি ১ ইউনিটে টিক2    for i in range(3):3        print("টিক", i)4        await asyncio.sleep(1)5async def report():6    time.sleep(3)            # ভুল! loop জমে যায়7    # await asyncio.sleep(3)  # ঠিক
একই সাথে ticker আর report চলছে
ঘড়ি
event loop
টিক ১-এর সময়
  1. শুরুটা স্বাভাবিক। ticker “টিক 0” ছাপল, তারপর await asyncio.sleep(1)-এ সরে দাঁড়াল — টাইমার ১-এ। loop এবার report চালাল।
  2. time.sleep — পুরো thread ঘুমায়। report-এর time.sleep(3) await নয়; সে loop-কে নিয়ন্ত্রণ ফেরত দেয় না। ১ ইউনিটে ticker-এর টাইমার বেজেছে, ticker প্রস্তুত — কিন্তু তাকে চালাবে যে loop, সে নিজেই ঘুমিয়ে আছে। ওয়েব সার্ভার হলে এই ৩ সেকেন্ড প্রত্যেক ব্যবহারকারীর পাতা আটকে থাকত, একজনের অনুরোধের কারণে।
  3. দেরিতে জেগে ওঠা। ৩-এ report শেষ হয়ে loop-কে নিয়ন্ত্রণ ফেরত দিল। loop দেখল ticker অনেক আগেই প্রস্তুত, সাথে সাথে চালাল — “টিক 1” এল ২ ইউনিট দেরিতে। কোনো error নেই, কোনো traceback নেই; শুধু সব কিছু রহস্যজনকভাবে ধীর। এজন্যই এই বাগ খুঁজে পাওয়া কঠিন।
  4. ঠিক রূপ — asyncio.sleep. await asyncio.sleep(3) লিখলে report-ও টাইমার বসিয়ে সরে দাঁড়ায়। loop মুক্ত থাকে, টিকগুলো ০, ১, ২-এ সময়মতো আসে, আর report শেষ হয় ৩-এ। একই ৩ ইউনিট অপেক্ষা, কিন্তু এবার অন্যদের আটকে না রেখে।

time.sleep শুধু একটি উদাহরণ। loop আটকায় এমন যেকোনো কিছু, যা await ছাড়া অপেক্ষা করে বা অনেকক্ষণ CPU দখল করে রাখে:

loop আটকায়async বিকল্প
time.sleep(n)await asyncio.sleep(n)
requests.get(url)async HTTP client — httpx.AsyncClient বা aiohttp (পর্ব ৪৭)
সাধারণ ডেটাবেস driver (যেমন sqlite3, psycopg2)async driver (যেমন asyncpg, aiosqlite), নইলে to_thread
বড় ফাইল পড়া, input()await asyncio.to_thread(...)
ভারী হিসাব (ছবি প্রসেস, বড় লুপ)process pool (পর্ব ৪১) — asyncio এখানে সাহায্য করে না

asyncio.to_thread — ব্লক করা কোডকে পাশের ঘরে পাঠানো

সব লাইব্রেরির async রূপ নেই। পুরোনো একটি পেমেন্ট SDK হয়তো শুধু সাধারণ ব্লক করা ফাংশন দেয়। তখন Python 3.9 থেকে আসা asyncio.to_thread কাজে লাগে: এটা ফাংশনটিকে আলাদা একটি thread-এ চালায় আর আপনাকে একটি awaitable দেয়। loop-এর thread মুক্ত থাকে, অন্য কাজ চলতে থাকে:

import asyncio, time

def legacy_payment(amount):      # পুরোনো, ব্লক করা লাইব্রেরি
    time.sleep(2)
    return f"{amount} টাকা পরিশোধিত"

async def main():
    receipt = await asyncio.to_thread(legacy_payment, 500)
    print(receipt)

I/O-র অপেক্ষার জন্য এটা চমৎকার, কারণ অপেক্ষার সময় CPython-এর GIL ছেড়ে দেওয়া হয়। কিন্তু ভারী Python হিসাব to_thread-এ পাঠালে সাধারণ CPython-এ GIL-এর কারণে খুব একটা লাভ হয় না; সেখানে loop.run_in_executor-এ একটি ProcessPoolExecutor দিন, যা পর্ব ৪১-এর process-এর জগৎ।

ডিবাগ মোড: asyncio.run(main(), debug=True) (বা environment variable PYTHONASYNCIODEBUG=1) চালালে asyncio কোনো কাজ loop-কে ১০০ মিলিসেকেন্ডের বেশি আটকে রাখলে সতর্ক করে, আর never-awaited coroutine কোথায় তৈরি হয়েছিল তা দেখায়। “অ্যাপ মাঝে মাঝে জমে যায়” ধরনের রহস্য সমাধানে প্রথম পদক্ষেপ এটাই।

৬ · timeout আর cancellation — অপেক্ষার সীমা টানা

নেটওয়ার্কের জগতে একটি কঠিন সত্য: কিছু সার্ভার কখনোই উত্তর দেয় না। একটি bKash-ধাঁচের পেমেন্ট গেটওয়ে চাপে পড়ে ৩০ সেকেন্ড চুপ থাকতে পারে। সীমা না টানলে আপনার কাজও চিরকাল অপেক্ষা করবে। তাই প্রতিটি বাইরের অনুরোধে একটি timeout দেওয়া ভালো অভ্যাস।

Python 3.11 থেকে সবচেয়ে পরিষ্কার উপায় asyncio.timeout — একটি async context manager:

async def slow_service():
    await asyncio.sleep(1)           # ধীর সার্ভিস (নকল)
    return "উত্তর"

async def main():
    try:
        async with asyncio.timeout(0.05):
            result = await slow_service()
    except TimeoutError:
        result = "সার্ভিস সাড়া দেয়নি — আগের রেট দেখাচ্ছি"
    print(result)

ছাপা হয় সার্ভিস সাড়া দেয়নি — আগের রেট দেখাচ্ছি. সময় পেরোলে asyncio ভেতরের কাজটিকে cancel করে (await-এর জায়গায় CancelledError ঢোকায়), তারপর ব্লকের বাইরে সেটাকে built-in TimeoutError-এ বদলে দেয়। পুরোনো কোডে একই কাজ দেখবেন await asyncio.wait_for(slow_service(), timeout=0.05) দিয়ে — এক কাজের জন্য এটাও ঠিক আছে; asyncio.timeout-এর সুবিধা হলো একই সীমার ভেতরে কয়েকটি await রাখা যায়।

cancellation — কাজ থামানোর ভদ্র উপায়

task.cancel() একটি Task-কে থামতে বলে। এটা জোর করে মেরে ফেলা নয়: Task যে await-এ থেমে আছে, সেখানে একটি CancelledError ঢোকানো হয়। ফলে ভেতরের finally আর async with পরিষ্কারের সুযোগ পায় — সংযোগ বন্ধ, অর্ধেক লেখা ফাইল মুছে ফেলা। দুটি নিয়ম মনে রাখুন:

  • CancelledError গিলে ফেলবেন না। Python 3.8 থেকে এটা BaseException-এর সন্তান, যাতে সাধারণ except Exception একে ধরে না ফেলে। ধরলে পরিষ্কার করে আবার raise করুন; নইলে timeout আর TaskGroup ঠিকমতো কাজ করবে না।
  • cancel হয় শুধু await-এ। যে কাজ await ছাড়া লম্বা লুপ চালাচ্ছে, তাকে cancel করার সুযোগই আসে না — আবার সেই cooperative নিয়ম।

৭ · Queue, Semaphore, async with আর async for

asyncio.Queue — রান্নাঘরের অর্ডার-বোর্ড

অনেক বাস্তব সিস্টেমে একদল কাজ তৈরি করে (producer), আরেকদল কাজ সারে (consumer). রেস্টুরেন্টে ওয়েটার অর্ডার বোর্ডে লেখেন, রাঁধুনিরা বোর্ড থেকে একটি একটি অর্ডার তুলে রাঁধেন। এই producer–consumer নকশার জন্য asyncio দেয় asyncio.Queue: await queue.get() বোর্ড খালি থাকলে অপেক্ষা করে (loop আটকে না রেখে), queue.task_done() জানায় একটি অর্ডার শেষ, আর await queue.join() অপেক্ষা করে যতক্ষণ না সব অর্ডার শেষ। নিচে দুজন রাঁধুনি, চারটি অর্ডার:

import asyncio

async def cook(name, orders, done):
    while True:
        dish, units = await orders.get()
        await asyncio.sleep(units * 0.05)   # রান্নার সময় (নকল)
        done.append(f"{name} → {dish}")
        orders.task_done()

async def main():
    orders, done = asyncio.Queue(), []
    for item in [("খিচুড়ি", 5), ("চা", 1), ("পরোটা", 2), ("ডিম", 1)]:
        orders.put_nowait(item)
    cooks = [asyncio.create_task(cook(n, orders, done)) for n in ("রহিম", "করিম")]
    await orders.join()                     # সব অর্ডার শেষ হওয়া পর্যন্ত
    for c in cooks:
        c.cancel()                          # রাঁধুনিদের ছুটি
    print(*done, sep="\n")

asyncio.run(main())

আউটপুট চার লাইন: করিম → চা, করিম → পরোটা, করিম → ডিম, রহিম → খিচুড়ি. হিসাবটা ইউনিটে করুন: শুরুতে রহিম তুললেন খিচুড়ি (৫ ইউনিট), করিম চা (১)। ১-এ করিম চা শেষ করে পরোটা তুললেন, ৩-এ শেষ করে ডিম, ৪-এ ডিম শেষ। রহিমের খিচুড়ি শেষ ৫-এ। যে রাঁধুনি আগে মুক্ত, তিনিই পরের অর্ডার তোলেন — তাই কাজ নিজে থেকেই ভাগ হয়ে যায়, কাউকে আগে থেকে ঠিক করে দিতে হয় না। cook-এর while True চিরকাল চলত, তাই join()-এর পর আমরা Task দুটি cancel করেছি: তারা orders.get()-এ অপেক্ষা করছিল, সেখানেই CancelledError পেয়ে থেমেছে। একটি thread, কোনো lock নেই, তবু done list-এ কোনো গোলমাল নেই — কারণ দুটি await-এর মাঝে কেউ বাধা দেয় না।

বাস্তবে queue-তে একটি সর্বোচ্চ আকার দেওয়া ভালো: asyncio.Queue(maxsize=100). তখন বোর্ড ভরে গেলে await queue.put(...) অপেক্ষা করে — producer আপনা থেকেই ধীর হয়, consumer-দের ডুবিয়ে দেয় না। একে বলে backpressure. (Python 3.13 থেকে queue.shutdown()-ও আছে, যা cancel ছাড়াই consumer-দের থামানোর একটি গোছানো উপায়।)

Semaphore — একসাথে কতজন, তার সীমা

ধরুন একটি কোচিং সেন্টার ১০ জন শিক্ষার্থীকে পরীক্ষার ফল SMS-এ পাঠাবে। SMS গেটওয়ে বলে দিয়েছে, একসাথে ৩টির বেশি অনুরোধ পাঠালে আপনাকে আটকে দেবে (rate limiting). gather দিয়ে ১০টি একসাথে ছাড়লে গেটওয়ে বিরক্ত হবে। asyncio.Semaphore(3) হলো তিনটি টোকেনের একটি বাক্স: async with sem: একটি টোকেন নেয়, ব্লক শেষে ফেরত দেয়; টোকেন না থাকলে অপেক্ষা করে:

async def send_sms(user, sem):
    async with sem:                       # টোকেন নাও — না থাকলে অপেক্ষা
        await asyncio.sleep(0.01 * (user % 3 + 1))   # গেটওয়ের অপেক্ষা
        return f"user-{user}: পাঠানো হলো"

async def main():
    sem = asyncio.Semaphore(3)            # একসাথে সর্বোচ্চ ৩টি
    results = await asyncio.gather(*(send_sms(u, sem) for u in range(1, 11)))
    print(len(results), results[0])       # 10 user-1: পাঠানো হলো

এখানে ১০টি Task-ই একসাথে তৈরি হয়, কিন্তু গেটওয়ে পর্যন্ত একসাথে পৌঁছায় সর্বোচ্চ ৩টি; একটি শেষ হলেই পরেরটি টোকেন পায়। ওয়েব scraping-এ এটা শুধু কৌশল নয়, দায়িত্ব: অন্যের সার্ভারে একসাথে শত শত অনুরোধ পাঠালে সেটা কার্যত আক্রমণের মতো দেখায়। সীমিত concurrency, অনুরোধের মাঝে বিরতি আর সাইটের নিয়ম (robots.txt, ব্যবহারের শর্ত) মানা — পর্ব ৪৭-এ বিস্তারিত।

async with আর async for

পর্ব ৩৬-এ দেখেছেন, with ব্লকের শুরুতে __enter__ আর শেষে __exit__ চলে। কিন্তু নেটওয়ার্ক সংযোগ খোলা বা বন্ধ করাও অপেক্ষার কাজ — সেখানে await দরকার। তাই আছে async context manager: __aenter__ আর __aexit__, যারা coroutine; লেখা হয় async with. Semaphore, TaskGroup, asyncio.timeout, httpx.AsyncClient() — সবাই এভাবে কাজ করে। contextlib.asynccontextmanager দিয়ে generator-এর মতো করে নিজেরটাও লেখা যায়।

একইভাবে, পর্ব ৩৪-এর iterator-এর async রূপ হলো async iterator: __anext__ একটি coroutine, আর লুপটি async for. প্রতিটি পরের জিনিসের জন্য অপেক্ষা করা যায় — যেমন একটি API-র পাতা একটির পর একটি আনা, বা চ্যাট-বটে আসা মেসেজ পড়া। async def-এর ভেতরে yield লিখলেই একটি async generator (Python 3.6 থেকে, PEP 525):

async def pages(total):                # async generator
    for p in range(1, total + 1):
        await asyncio.sleep(0.01)      # পরের পাতার অপেক্ষা (নকল)
        yield f"পাতা {p}"

async def main():
    async for page in pages(3):
        print(page)                    # পাতা 1, পাতা 2, পাতা 3

৮ · কোথায় asyncio, আর কখন নয় — thread আর process-এর পাশে

আজকের Python জগতে asyncio চোখে না পড়লেও সবখানে আছে:

ওয়েব সার্ভার

FastAPI-তে async def endpoint লিখলে একটি process হাজারো অনুরোধ সামলায়, প্রতিটি ডেটাবেস বা অন্য API-র অপেক্ষায় থাকার সময় অন্যরা চলে। পর্ব ৪৮-এ হাতে-কলমে।

FastAPI, Starlette, uvicorn

HTTP client আর API

অনেকগুলো API একসাথে ডাকা — দাম তুলনা, রেট আনা, AI মডেলের API-তে অনেক অনুরোধ। httpx আর aiohttp; পর্ব ৪৭-এ।

httpx, aiohttp

বট আর রিয়েল-টাইম

Telegram বা Discord বট সারাক্ষণ মেসেজের অপেক্ষায় থাকে, একসাথে অনেক চ্যাট সামলায়। WebSocket চ্যাট, লাইভ স্কোর আপডেটও একই ধাঁচের।

python-telegram-bot, discord.py

scraper আর ডেটা সংগ্রহ

অনেক পাতা একসাথে আনা — তবে Semaphore দিয়ে সীমিত গতিতে, সাইটের নিয়ম মেনে। দায়িত্বশীল scraping পর্ব ৪৭-এ।

দায়িত্বের সাথে

asyncio, thread না process — কীভাবে বাছবেন

পর্ব ৪১-এ thread আর process দেখেছেন। তিনটিকে পাশাপাশি রাখলে সিদ্ধান্তটি সহজ হয়:

বিষয়asynciothreadprocess
সবচেয়ে উপযুক্তঅনেক I/O অপেক্ষা (শত-হাজার সংযোগ)কিছু I/O, বিশেষ করে ব্লক করা লাইব্রেরির সাথেCPU-র ভারী হিসাব
কাজ বদল কে করেকাজ নিজে, await-এ (cooperative)অপারেটিং সিস্টেম, যেকোনো সময়অপারেটিং সিস্টেম
একাধিক core ব্যবহারনাসাধারণ CPython-এ GIL-এর কারণে সীমিত; free-threaded build-এ হ্যাঁহ্যাঁ
প্রতি কাজের খরচখুব কম (একটি ছোট object)মাঝারি (নিজস্ব stack)বেশি (আলাদা মেমরি)
প্রধান বিপদloop আটকে দেওয়া, await ভুলে যাওয়াrace condition, lock-এর জটডেটা আদান-প্রদানের খরচ

একটি সহজ নিয়ম: অপেক্ষা বেশি আর async লাইব্রেরি আছে → asyncio. অপেক্ষা বেশি কিন্তু লাইব্রেরি শুধু ব্লক করা → thread (বা asyncio-র ভেতরে to_thread). হিসাব বেশি → process. আর মাত্র তিন-চারটি অনুরোধ, প্রোগ্রাম একবার চলে থামে — তাহলে হয়তো কোনোটিই নয়; সরল sync কোডই সবচেয়ে ভালো। asyncio পুরো কোডবেসে ছড়ায় (ভাবনার প্রশ্ন ৩), তাই দরকার ছাড়া আনবেন না।

C/C++ জানলে

C-তে ভাষার ভেতরে কোনো async নেই; একই কাজ করা হয় হাতে event loop লিখে — select, poll, epoll দিয়ে, প্রতিটি ঘটনায় একটি callback ডেকে। Node.js-এর ভিত্তি libuv আর nginx এভাবেই লেখা, আর Python-এর দ্রুত বিকল্প loop uvloop-ও libuv-এর ওপর তৈরি। C++20-এ ভাষায় coroutine এসেছে (co_await, co_return), কিন্তু standard library কোনো তৈরি event loop দেয় না — Boost.Asio-র মতো লাইব্রেরি লাগে। C++-এর std::async আর std::future সাধারণত thread-এ চলে, Python-এর asyncio-র মতো এক thread-এর loop নয়। বিস্তারিত দেখুন C++ কোর্সের async ও future পর্বে।

পরীক্ষা ও ইন্টারভিউতে
  • concurrency আর parallelism-এর পার্থক্য? — concurrency: অনেক কাজ একই সময়সীমায় পালা করে এগোয় (এক ওয়েটার); parallelism: অনেক কাজ একই মুহূর্তে সত্যিই চলে (অনেক core). asyncio প্রথমটি দেয়।
  • async def ফাংশন await ছাড়া ডাকলে কী হয়? — body চলে না, শুধু coroutine object তৈরি হয়; পরে “coroutine ... was never awaited” RuntimeWarning আসে।
  • coroutine-এর ভেতরে time.sleep কেন ভুল? — এটা পুরো thread ঘুম পাড়ায়, তাই event loop আর অন্য সব কাজও থেমে যায়; await asyncio.sleep লাগবে।
  • gather আর TaskGroup-এর পার্থক্য? — gather-এ একটি ব্যর্থ হলে বাকিরা চলতে থাকে; TaskGroup বাকিদের cancel করে সব ভুল ExceptionGroup-এ দেয়, আর ব্লক ছাড়ার আগে সব Task শেষ নিশ্চিত করে।
  • asyncio কি CPU-র কাজ দ্রুত করে? — না; এক thread-এ চলে, শুধু অপেক্ষার সময় কাজে লাগায়। CPU কাজে process লাগে।

৯ · AI-এর যুগে — AI-এর লেখা async কোড যাচাই

async কোড AI-এর জন্য বিশেষভাবে কঠিন, কারণ ভুল কোডও প্রায়ই চলে — শুধু ধীরে, বা কিছু কাজ নিঃশব্দে বাদ দিয়ে। ছোট উদাহরণে সব ঠিক দেখায়; সমস্যা ধরা পড়ে চাপের মুখে, যখন ১০০ জন ব্যবহারকারী একসাথে আসে। AI-এর লেখা async কোডে বারবার যা দেখা যায়:

AI প্রায়ই যা লেখেসমস্যাভালো রূপ
save_to_db(data) — await ছাড়াকাজটি কখনো চলে না; শুধু RuntimeWarningawait save_to_db(data)
async def-এর ভেতরে requests.get, time.sleepপুরো loop আটকে যায়; সব ব্যবহারকারী অপেক্ষায়httpx.AsyncClient, asyncio.sleep, বা to_thread
লুপের ভেতরে একটি একটি করে await fetch(u)async হয়েও পরপর চলে; সময় = যোগফলTaskGroup বা gather
asyncio.create_task(job()), ফল কোথাও রাখা নেইTask মাঝপথে মুছে যেতে পারে; ভুল হারিয়ে যায়TaskGroup, বা reference একটি set-এ রাখা
১০,০০০ URL একসাথে gather-এসার্ভারে চাপ, ব্লক হওয়া, নিজের মেমরি শেষSemaphore বা নির্দিষ্ট সংখ্যক worker আর Queue
ভারী হিসাব async def-এ রেখে “দ্রুত করা”কোনো লাভ নেই, উল্টো loop আটকায়process pool (পর্ব ৪১)
except Exception: pass async কোডেভুল চাপা পড়ে; পুরোনো Python-এ cancel-ও গিলে ফেলতনির্দিষ্ট exception, আর CancelledError আবার raise

AI-কে async কোড লিখতে বললে কাজের ধরন আর সীমাগুলো আগেই বলে দিন। নিচের কাঠামোটি কপি করে ব্যবহার করুন:

আমি Python 3.12 বা নতুন version ব্যবহার করি। ২০০টি পণ্যের দাম তিনটি ভিন্ন দোকানের API থেকে এনে সবচেয়ে কম দামটি বের করার async কোড দাও (প্রতিটি API উত্তর দিতে ০.২–২ সেকেন্ড নেয়, মাঝে মাঝে একদম উত্তর দেয় না)। শর্ত: ১. HTTP-র জন্য httpx.AsyncClient ব্যবহার করো, একটি client পুরো প্রোগ্রামে; requests, time.sleep বা অন্য কোনো ব্লক করা কল async def-এর ভেতরে রাখবে না। ২. প্রতিটি coroutine ডাকার জায়গায় await আছে কি না নিজে পরীক্ষা করো। ৩. একসাথে সর্বোচ্চ ১০টি অনুরোধ — asyncio.Semaphore দিয়ে সীমা টানো; প্রতিটি অনুরোধে ৫ সেকেন্ডের timeout (asyncio.timeout). ৪. কাজগুলো asyncio.TaskGroup দিয়ে চালাও; কোনো create_task-এর ফল reference ছাড়া ফেলে রাখবে না। ৫. একটি দোকান ব্যর্থ হলে পুরো প্রোগ্রাম যেন না থামে — সেই পণ্যের জন্য বাকি দোকানের দাম নাও, আর শেষে কতগুলো অনুরোধ ব্যর্থ হলো জানাও। CancelledError গিলে ফেলবে না। ৬. কোনো ভারী CPU হিসাব থাকলে বলো সেটা কেন asyncio-তে দ্রুত হবে না। ৭. কোডের নিচে বাংলায় ৩ লাইনে বলো, কোন জায়গাগুলোতে event loop অন্য কাজে যেতে পারে।

AI-এর লেখা async কোড পেলে — যাচাইয়ের চেকলিস্ট

ভাবনার প্রশ্ন

প্রতিটি প্রশ্ন নিয়ে নিজে কিছুক্ষণ ভাবুন — তারপর “→ উত্তর” চাপুন। এগুলো আপনাকে কোড-লেখক থেকে সিস্টেম-চিন্তক বানাবে।

প্র ০১Python-এ asyncio standard library-তে এল ২০১৪ সালে, অথচ async নেটওয়ার্ক প্রোগ্রামিং তার অনেক আগে থেকেই হচ্ছিল। Twisted, Tornado আর generator-ভিত্তিক coroutine থেকে PEP 3156 পর্যন্ত যাত্রাটা কেমন ছিল, আর একটি “standard” loop কেন দরকার হলো?

২০০০-এর দশকের শুরুতে Python-এ async নেটওয়ার্ক প্রোগ্রামিংয়ের প্রধান নাম ছিল Twisted, যার শুরু Glyph Lefkowitz-এর হাতে। Twisted-এর মূল ধারণা ছিল event loop (তারা বলত “reactor”) আর callback: একটি অনুরোধ পাঠিয়ে বলে দেওয়া, “উত্তর এলে এই ফাংশন ডেকো।” এর ওপর ছিল Deferred object, অনেকটা আজকের Future-এর মতো। Twisted শক্তিশালী ছিল, কিন্তু callback-এর পর callback জুড়ে লেখা কোড পড়া কঠিন হয়ে যেত — যুক্তি ছড়িয়ে থাকত অনেক ছোট ফাংশনে, আর error সামলানো ছিল আলাদা একটি callback-শৃঙ্খল।

২০০৬ সালে Python 2.5-এ PEP 342 generator-কে দুইমুখী করল (send(), throw())। লোকেরা দ্রুত বুঝল, একটি generator yield করে “আমি এই অনুরোধের অপেক্ষায়” বলতে পারে, আর loop উত্তর এলে send() দিয়ে তাকে আবার চালাতে পারে — callback-এর জট ছাড়াই, কোড দেখতে সাধারণ ক্রমিক কোডের মতো। Twisted-এর inlineCallbacks আর FriendFeed-এর তৈরি, ২০০৯ সালে Facebook-এর ওপেন সোর্স করা Tornado এই কৌশল জনপ্রিয় করে। অন্যদিকে gevent-এর মতো লাইব্রেরি greenlet দিয়ে সাধারণ কোডকেই আড়ালে async বানাত।

সমস্যা হলো, প্রতিটি framework-এর নিজস্ব loop, নিজস্ব Future, নিজস্ব লাইব্রেরি। Twisted-এর জন্য লেখা ডেটাবেস driver Tornado-তে চলত না। Guido van Rossum ২০১২–১৩ সালে এই বিভাজন দূর করতে কাজ শুরু করেন — প্রকল্পের ডাকনাম ছিল “Tulip”. ফল PEP 3156: একটি সাধারণ event loop interface, Future, Task আর transport/protocol-এর নকশা, যাতে বিভিন্ন framework একই loop ভাগ করে নিতে পারে। Python 3.4-এ (২০১৪) এটা asyncio নামে standard library-তে এল, তখনো coroutine লেখা হতো yield from দিয়ে। এক বছর পর PEP 492 আনল async/await.

একটি standard loop-এর লাভ হলো সাধারণ ভিত্তি: আজ httpx, asyncpg, FastAPI, discord.py — ভিন্ন দলের লেখা লাইব্রেরি একই প্রোগ্রামে মিলেমিশে চলে, কারণ সবাই একই asyncio loop আর একই await বোঝে। Tornado নিজেও পরে asyncio-র loop-এর ওপর চলতে শুরু করে। তবে standard library-র নকশা চিরকালের জন্য বাঁধা পড়ে, তাই ভুলগুলোও টিকে থাকে — যেমন শুরুর দিকের API-র কিছু অংশ এখন পুরোনো ধাঁচের বলে গণ্য, আর Trio-র মতো বিকল্প লাইব্রেরি আলাদা পথে হেঁটেছে।

মূল উপলব্ধি: একটি ভাষার ইকোসিস্টেমে সবচেয়ে মূল্যবান জিনিস প্রায়ই সবচেয়ে সেরা টুল নয়, বরং সবার মানা একটি সাধারণ interface. asyncio-র আসল সাফল্য তার গতি নয় — এটা Python-এর ভাঙা async জগৎকে একটি ভাষায় কথা বলিয়েছে।

প্র ০২Python 3.5-এ PEP 492 async def আর await আনল, যদিও yield from দিয়ে একই কাজ আগেই করা যেত। আলাদা syntax কেন দরকার হলো, আর সেটা কীভাবে ধাপে ধাপে ভাষায় ঢুকল?

Python 3.4-এর asyncio-তে একটি coroutine দেখতে ছিল সাধারণ generator-এর মতো: @asyncio.coroutine decorator, আর ভেতরে yield from asyncio.sleep(1). যন্ত্রটা কাজ করত, কিন্তু কয়েকটি গুরুতর অসুবিধা ছিল। প্রথমত, একটি ফাংশন generator না coroutine, তা দেখে বোঝা যেত না; decorator ভুলে গেলেও অনেক সময় কোড চলত, আর ভুল ধরা পড়ত অনেক পরে। দ্বিতীয়ত, ভুল করে coroutine-এর ওপর for লুপ চালানো যেত, বা generator-কে await করা যেত — দুটি ভিন্ন ধারণা একই object-এ মিশে ছিল। তৃতীয়ত, with আর for-এর async রূপ লেখার কোনো সরাসরি উপায় ছিল না: সংযোগ খোলা বা পরের রেকর্ড আনার মাঝে অপেক্ষা করা যেত না।

২০১৫ সালে Yury Selivanov PEP 492 প্রস্তাব করেন। এতে এল async def (যা দিয়ে native coroutine তৈরি হয়, একটি আলাদা ধরনের object), await (yield from-এর জায়গায়, কিন্তু শুধু awaitable-এর ওপর), আর async with ও async for (নতুন dunder method __aenter__, __aexit__, __aiter__, __anext__)। প্রস্তাবটি আশ্চর্য দ্রুত গৃহীত হয় আর Python 3.5-এই যোগ হয়। পরের বছর Python 3.6-এ তিনি PEP 525 দিয়ে async generator আনেন, আর PEP 530 আনে async comprehension.

পুরোনো কোড যাতে না ভাঙে, তাই পরিবর্তন এল ধাপে ধাপে। async আর await নাম হয়তো কারও কোডে variable হিসেবে ছিল, তাই 3.5 আর 3.6-এ এগুলো শুধু নির্দিষ্ট প্রসঙ্গে keyword-এর মতো আচরণ করত; Python 3.7-এ এরা পূর্ণ keyword হয়। (একটি মজার পরিণতি: জনপ্রিয় কিছু লাইব্রেরিতে async নামে parameter ছিল, সেগুলোকে নাম বদলাতে হয়েছিল।) পুরোনো @asyncio.coroutine decorator ধীরে ধীরে deprecated হয়ে Python 3.11-এ সরিয়ে ফেলা হয়। একই সময়ে asyncio-র API-ও সহজ হতে থাকে: 3.7-এ asyncio.run আর create_task, যাতে নতুনদের আর loop object নিয়ে মাথা ঘামাতে না হয়।

আলাদা syntax-এর আসল লাভ পড়ার সুবিধায়। async def দেখামাত্র পাঠক জানেন, এই ফাংশন ডাকলে সাথে সাথে চলবে না; await দেখামাত্র জানেন, এখানে অন্য কাজ ঢুকে পড়তে পারে। অন্য ভাষাও একই পথ নিয়েছে: C# ২০১২ সালে async/await আনে, তারপর JavaScript (ES2017), Rust, Kotlin-এর ধারণাও কাছাকাছি।

মূল উপলব্ধি: যন্ত্র এক হলেও উদ্দেশ্য আলাদা হলে আলাদা নাম দেওয়াই ভালো। yield from আর await ভেতরে প্রায় একই কাজ করে, কিন্তু আলাদা শব্দ পাঠককে প্রথম লাইনেই বলে দেয় কোডটি কোন জগতের — আর ভালো ভাষা-নকশার বড় অংশই হলো ভুল করা কঠিন বানানো।

প্র ০৩Bob Nystrom ২০১৫ সালে “What Color is Your Function?” লেখায় async ফাংশনকে “লাল” আর সাধারণ ফাংশনকে “নীল” বলে একটি সমস্যা দেখান। সমস্যাটি কী, Python-এ এটা কীভাবে দেখা দেয়, আর অন্য ভাষাগুলো কী সমাধান নিয়েছে?

Google-এ Dart ভাষার দলের সদস্য Bob Nystrom একটি কাল্পনিক ভাষার কথা লেখেন, যেখানে প্রতিটি ফাংশনের একটি রং আছে — লাল বা নীল। নিয়মগুলো: লাল ফাংশন ডাকা যায় শুধু আরেকটি লাল ফাংশনের ভেতর থেকে; লাল ফাংশন ডাকার নিয়ম বেশি ঝামেলার; আর অনেক গুরুত্বপূর্ণ লাইব্রেরি ফাংশন লাল। ফলাফল: কোথাও গভীরে একটি লাল ফাংশন ডাকার দরকার হলে তার ওপরের প্রতিটি ফাংশনকে লাল বানাতে হয় — রং ওপরের দিকে ছড়িয়ে পড়ে। তারপর তিনি বলেন, এই কাল্পনিক ভাষাটি আসলে async/await থাকা যেকোনো ভাষা।

Python-এ ঠিক এটাই ঘটে। async def ফাংশন লাল: তাকে await করা যায় শুধু আরেকটি async def থেকে। একটি পুরোনো sync ওয়েব অ্যাপে কোথাও গভীরে async HTTP client ব্যবহার করতে চাইলে তার caller, তার caller, … সবাইকে async বানাতে হয়, নয়তো মাঝখানে asyncio.run ডাকতে হয় — যা আগে থেকে loop চলছে এমন জায়গায় চলে না। এজন্য Python ইকোসিস্টেম কার্যত দুই ভাগ হয়েছে: requests বনাম httpx/aiohttp, psycopg2 বনাম asyncpg, Flask বনাম FastAPI. অনেক লাইব্রেরি দুটি রূপ রাখে (httpx-এ Client আর AsyncClient), যার মানে দ্বিগুণ রক্ষণাবেক্ষণ। এই সমস্যার উল্টো দিকও আছে: নীল ফাংশন থেকে একটি ব্লক করা কাজ লাল জগতে ঢুকলে loop আটকায় — অংশ ৫-এর বাগ।

অন্য ভাষাগুলো ভিন্ন পথ নিয়েছে। Go-তে কোনো রং নেই: প্রতিটি ফাংশন সাধারণ, আর go f() লিখে একটি হালকা goroutine চালানো যায়; runtime নিজেই I/O-র অপেক্ষায় goroutine বদলায়। Java-র Project Loom (Java 21-এ virtual thread) একই ধারণা আনল — ব্লক করা সাধারণ কোড লিখুন, runtime আড়ালে async করবে। Python-এ gevent এই পথ নিয়েছিল, standard library-র socket-কে “patch” করে। অন্যদিকে রং থাকার পক্ষেও যুক্তি আছে: await স্পষ্ট দেখায় কোথায় কাজ বদল হতে পারে, তাই দুটি await-এর মাঝের কোড নিশ্চিন্তে লেখা যায়। Twisted-এর লেখক Glyph একটি লেখায় ঠিক এই যুক্তি দিয়েছিলেন যে অদৃশ্য কাজ-বদল (implicit) বড় সিস্টেমে বাগ লুকায়।

মূল উপলব্ধি: প্রতিটি নকশা কোথাও না কোথাও দাম নেয়। স্পষ্ট await কোডের যুক্তি পড়া সহজ করে, কিন্তু পুরো কোডবেসকে দুই রঙে ভাগ করে; অদৃশ্য কাজ-বদল ভাগ দূর করে, কিন্তু কোথায় কাজ বদলায় তা লুকিয়ে ফেলে। asyncio বাছাই করা মানে এই দাম জেনেশুনে দেওয়া।

প্র ০৪Nathaniel J. Smith ২০১৮ সালে “Notes on structured concurrency, or: Go statement considered harmful” লেখায় দাবি করেন যে “শুরু করে ভুলে যাও” ধাঁচের task তৈরি goto-র মতোই ক্ষতিকর। যুক্তিটা কী, আর সেটা কীভাবে Python 3.11-এর TaskGroup পর্যন্ত পৌঁছাল?

১৯৬৮ সালে Edsger Dijkstra-র বিখ্যাত চিঠি “Go To Statement Considered Harmful” যুক্তি দেয়: goto যেকোনো জায়গায় লাফ দিতে দেয়, তাই কোডের একটি অংশ দেখে বোঝা যায় না নিয়ন্ত্রণ কোথা থেকে আসছে বা কোথায় যাচ্ছে। এর জবাবে আসে structured programming — if, loop, ফাংশন — যেখানে প্রতিটি ব্লকের একটি প্রবেশ আর একটি প্রস্থান। ফাংশন ডাকলে সে শেষ হয়ে ফিরে আসবেই; এই নিশ্চয়তার ওপর exception, with, ক্লিনআপ — সবকিছু দাঁড়িয়ে।

Nathaniel J. Smith দেখান, Go-র go f(), JavaScript-এর promise ছেড়ে দেওয়া, বা asyncio-র create_task — এরা সবাই এই নিশ্চয়তা ভাঙে। একটি ফাংশন ফিরে এল, কিন্তু তার শুরু করা কাজ পেছনে চলছে। সেই কাজে exception হলে কে ধরবে? ফাংশনের with ব্লক রিসোর্স বন্ধ করে দিল, অথচ পেছনের কাজ তখনো সেটা ব্যবহার করছে। প্রোগ্রাম বন্ধের সময় কোন কাজ শেষ হলো আর কোনটি মাঝপথে কাটা পড়ল, কেউ জানে না। তাঁর ভাষায়, এটা concurrency-র goto.

তাঁর সমাধান ছিল nursery, যা তিনি নিজের লেখা async লাইব্রেরি Trio-তে (২০১৭ সালে প্রথম প্রকাশ) চালু করেন। নতুন কাজ শুরু করা যায় শুধু একটি nursery ব্লকের ভেতরে, আর ব্লক থেকে বের হওয়া যায় শুধু তখন, যখন সব কাজ শেষ। একটি কাজ ব্যর্থ হলে বাকিদের cancel করা হয়, আর ভুলটি ব্লকের বাইরে আসে — ঠিক একটি সাধারণ ফাংশন-ডাকার মতো। “structured concurrency” শব্দটি এর আগে Martin Sústrik তাঁর C লাইব্রেরি libdill-এর প্রসঙ্গে ব্যবহার করেছিলেন; Smith-এর লেখা ধারণাটিকে ব্যাপক পরিচিতি দেয়। Kotlin-এর coroutine scope, Swift-এর task group, Java-র StructuredTaskScope — অনেক ভাষা এর পর একই পথে হেঁটেছে।

একাধিক কাজ একসাথে ব্যর্থ হলে একটি exception যথেষ্ট নয় — এজন্য Trio-র অভিজ্ঞতা থেকে Python-এ এল PEP 654-এর ExceptionGroup আর except*. দুটিই Python 3.11-এ যোগ হয়, আর তাদের ওপর ভিত্তি করে asyncio পায় TaskGroup আর asyncio.timeout — যাদের নকশা Trio-র nursery আর cancel scope-এর সরাসরি উত্তরসূরি। পুরোনো gather আর create_task রয়ে গেছে, কিন্তু নতুন কোডের জন্য ডকুমেন্টেশন TaskGroup-কেই সুপারিশ করে।

মূল উপলব্ধি: নিয়ন্ত্রণের প্রবাহ কোডের আকারের সাথে মিললে মানুষের পক্ষে প্রোগ্রাম বোঝা সম্ভব হয়। ৫০ বছর আগে goto-র বিরুদ্ধে যে যুক্তি জিতেছিল, সেটাই আজ concurrency-তে ফিরে এসেছে — শুধু লাফের বদলে “পেছনে চলতে থাকা কাজ”.

প্র ০৫JavaScript আর Node.js-ও একটি event loop-এর ওপর চলে, আর সেখানে async কোড Python-এর চেয়ে অনেক বেশি স্বাভাবিক। দুটির loop কোথায় আলাদা, আর কেন কোনোটিই CPU-র ভারী কাজ দ্রুত করতে পারে না?

ব্রাউজারের JavaScript জন্ম থেকেই এক thread-এর event loop-এ চলে: বোতাম চাপা, টাইমার, নেটওয়ার্কের উত্তর — সবই ঘটনা, আর প্রতিটির জন্য একটি callback. ২০০৯ সালে Ryan Dahl একই মডেল সার্ভারে আনেন Node.js দিয়ে; ভেতরে আছে Google-এর V8 engine আর C-তে লেখা libuv লাইব্রেরি, যা অপারেটিং সিস্টেমের epoll/kqueue-র ওপর loop চালায়। Node-এ loop সবসময় চলছে, আর প্রায় সব I/O API জন্ম থেকেই non-blocking. তাই সেখানে “রং”-এর সমস্যা কম চোখে পড়ে — পুরো ইকোসিস্টেমটাই লাল। callback-এর জট (“callback hell”) থেকে Promise (ES2015), তারপর async/await (ES2017) এসেছে — Python-এর যাত্রার সাথে অনেক মিল।

Python-এ উল্টো: ভাষা আর লাইব্রেরির বিশাল অংশ sync, আর loop হলো একটি লাইব্রেরি, যা আপনি asyncio.run দিয়ে স্পষ্টভাবে চালান। আরেকটি পার্থক্য: JavaScript-এ একটি async ফাংশন ডাকলেই সে চলতে শুরু করে আর একটি Promise দেয়, await না করলেও কাজটি হয়। Python-এ coroutine ডাকলে কিছুই চলে না যতক্ষণ না await বা Task — এজন্যই “never awaited” সতর্কবার্তা Python-এর নিজস্ব সমস্যা। Python-এর loop প্রতিস্থাপনও করা যায়: uvloop (Yury Selivanov-দের তৈরি) libuv-এর ওপর asyncio-র loop চালায়, অনেক ক্ষেত্রে বেশ দ্রুত।

কিন্তু দুটির সীমা একই: loop একটি thread-এ চলে, আর এক মুহূর্তে একটিই callback বা coroutine চলে। event loop কাজ দ্রুত করে না — সে শুধু অপেক্ষার সময় নষ্ট হতে দেয় না। ১০টি API ডাক, প্রতিটি ১ সেকেন্ড অপেক্ষা: পরপর ১০ সেকেন্ড, একসাথে প্রায় ১ সেকেন্ড, কারণ অপেক্ষাগুলো একটির ওপর আরেকটি পড়ে। কিন্তু ১০টি ছবি resize, প্রতিটি ১ সেকেন্ড CPU: এখানে অপেক্ষা নেই, CPU সারাক্ষণ ব্যস্ত, তাই loop-এর হাতে বদলানোর মতো ফাঁকা সময়ই নেই — ১০ সেকেন্ডই লাগবে, আর সেই সময় loop-এর বাকি সব কাজও আটকে থাকবে। Node এর জন্য worker thread আর আলাদা process দেয়; Python-এ ProcessPoolExecutor, আর free-threaded build-এ সত্যিকারের parallel thread (পর্ব ৪১)।

Amdahl-এর নীতির ভাষায়: কোনো কাজের যে অংশ অপেক্ষা, asyncio সেই অংশই সংকুচিত করতে পারে; হিসাবের অংশে তার কোনো প্রভাব নেই। তাই কোড “ধীর” মনে হলে আগে মাপুন — সময় কি অপেক্ষায় যাচ্ছে, না হিসাবে? পরের পর্বে ঠিক এই মাপার কৌশল দেখবেন।

মূল উপলব্ধি: event loop একটি সময়-ব্যবস্থাপক, শক্তি-বর্ধক নয়। JavaScript আর Python দুই ভিন্ন পথে একই জায়গায় পৌঁছেছে — I/O-র জন্য loop, হিসাবের জন্য একাধিক core. কোনটি কখন লাগবে, সেই বিচারই একজন প্রকৌশলীর আসল কাজ।

অনুশীলন

এগুলোর বেশিরভাগে নতুন কোড লিখতে হবে না — সময়রেখা আঁকা, অনুমান করা আর ভুল খোঁজাই আসল কাজ।

  1. সময়রেখা আঁকুন: চারটি কাজের অপেক্ষা ২, ৪, ১ আর ৩ ইউনিট। (ক) পরপর await দিলে মোট কত ইউনিট? (খ) gather দিলে? (গ) gather-এ শেষ হওয়ার ক্রম কী, আর ফলের list-এর ক্রম কী? (ঘ) Semaphore(2) দিয়ে একসাথে সর্বোচ্চ ২টি চললে (জমা দেওয়ার ক্রমে টোকেন পায়) মোট কত?

    (ক) ২ + ৪ + ১ + ৩ = ১০ ইউনিট। (খ) সবচেয়ে লম্বাটি — ৪ ইউনিট। (গ) শেষ হওয়ার ক্রম: ১-এর কাজ (১ ইউনিটে), ২-এর (২-এ), ৩-এর (৩-এ), ৪-এর (৪-এ); কিন্তু gather-এর ফল আসে জমা দেওয়ার ক্রমে — ২, ৪, ১, ৩-এর কাজগুলোর ফল সেই ক্রমেই। (ঘ) শুরুতে ২ আর ৪ চলে। ২-এ প্রথমটি শেষ, টোকেন পায় ১-এর কাজ — শেষ ৩-এ। ৩-এ টোকেন পায় ৩-এর কাজ — শেষ ৬-এ। ৪-এর কাজ শেষ ৪-এ। মোট ৬ ইউনিট — পরপরের চেয়ে অনেক কম, সব একসাথের চেয়ে একটু বেশি, আর সার্ভারের ওপর চাপ অর্ধেক।

  2. বাগ খুঁজুন: একটি async ওয়েব অ্যাপে ব্যবহারকারীরা অভিযোগ করছেন, “রিপোর্ট বোতাম কেউ চাপলেই পুরো সাইট ৫ সেকেন্ডের জন্য জমে যায়।” রিপোর্টের কোডে আছে async def make_report(): data = requests.get(API_URL).json(); .... কোনো error নেই। কারণ কী, আর দুটি ভিন্ন সমাধান বলুন।

    requests.get একটি ব্লক করা কল — async def-এর ভেতরে থাকলেও সে await করে না, পুরো thread-কে উত্তরের অপেক্ষায় আটকে রাখে। asyncio-তে thread একটিই, তাই সেই ৫ সেকেন্ড event loop অন্য কোনো ব্যবহারকারীর অনুরোধ ধরতে পারে না। সমাধান ১: async HTTP client — async with httpx.AsyncClient() as client: r = await client.get(API_URL). সমাধান ২ (লাইব্রেরি বদলানো না গেলে): await asyncio.to_thread(requests.get, API_URL), যাতে অপেক্ষাটি পাশের thread-এ হয়। আর ভবিষ্যতে ধরতে: debug মোডে চালালে asyncio “slow callback” সতর্কবার্তায় ঠিক এই জায়গাটি দেখাত।

  3. আউটপুট অনুমান করুন (একমাত্র কোড-পড়ার কাজ): async def f(): print("f চলল"); return 1 (আলাদা লাইনে)। async def main():-এর ভেতরে পরপর তিনটি লাইন: x = f(), print(type(x).__name__), print(await x). asyncio.run(main()) চালালে কী কী ছাপা হবে, কোন ক্রমে? আর await x লাইনটি মুছে দিলে কী বদলাবে?
    coroutine
    f চলল
    1

    x = f() শুধু coroutine object বানায়, “f চলল” তখনো ছাপা হয়নি — তাই প্রথম লাইন coroutine. await x-এ body চলে: “f চলল”, তারপর ফেরত মান 1 ছাপা হয়। await লাইনটি মুছলে শুধু coroutine ছাপা হবে; “f চলল” কখনো আসবে না, আর stderr-এ RuntimeWarning: coroutine 'f' was never awaited. শিক্ষা: coroutine ডাকা আর চালানো দুটি আলাদা ঘটনা।

  4. AI অডিট: একটি AI ১০,০০০ ব্যবহারকারীকে নোটিফিকেশন পাঠানোর এই কোড দিয়েছে: for u in users: asyncio.create_task(notify(u)), তারপর await asyncio.sleep(5) # সব শেষ হওয়ার জন্য. notify-এর ভেতরে time.sleep(0.1) দিয়ে “rate limit” করা। অন্তত চারটি সমস্যা খুঁজুন আর ভালো নকশা বলুন।

    (১) Task-এর reference নেই — create_task-এর ফল কোথাও রাখা হয়নি; কিছু Task মাঝপথে মুছে যেতে পারে, আর কোনোটির ভুল কেউ দেখবে না। (২) sleep(5) দিয়ে “অপেক্ষা” — অনুমাননির্ভর; ৫ সেকেন্ডে সব শেষ না হলে বাকিগুলো কাটা পড়বে, আগে শেষ হলে সময় নষ্ট। (৩) time.sleep(0.1) — loop আটকায়; ১০,০০০ × ০.১ = ১,০০০ সেকেন্ড সব কিছু ক্রমিকভাবে জমে থাকবে, আর পুরো প্রোগ্রাম ততক্ষণ অন্য কিছু করতে পারবে না। (৪) কোনো concurrency সীমা নেই — একসাথে ১০,০০০ সংযোগ খোলার চেষ্টা, নোটিফিকেশন সার্ভিস ব্লক করে দিতে পারে। (৫) timeout বা ভুল সামলানো নেই।

    ভালো নকশা: sem = asyncio.Semaphore(20); notify-এর ভেতরে async with sem: আর async with asyncio.timeout(5):, বিরতির জন্য await asyncio.sleep(...); সব কাজ async with asyncio.TaskGroup() as tg:-এর ভেতরে tg.create_task(notify(u)). ব্লক শেষ মানেই সব কাজ শেষ — অনুমানের দরকার নেই। একজনের ব্যর্থতায় বাকিরা যেন cancel না হয়, সেজন্য notify-এর ভেতরেই নিজের exception ধরে লগ করুন। খুব বড় সংখ্যায় ১০,০০০ Task-এর বদলে কয়েকটি worker আর একটি asyncio.Queue আরও কম মেমরিতে একই কাজ করে।

নিজেকে যাচাই করুন

ভুল হলে কারণসহ ব্যাখ্যা পাবেন, আবার চেষ্টাও করতে পারবেন।

ক্রমPython asyncio-তে await asyncio.sleep(1) লেখা একটি Task-এর জীবনের ধাপগুলো সঠিক ক্রমে সাজান (▲▼ চেপে সরান)

  1. 📥 Task তৈরি হয়ে event loop-এর ready তালিকায় ওঠে
  2. ▶️ loop Task-টি চালায়, সে await পর্যন্ত চলে
  3. ⏰ await-এ Task একটি টাইমার বসিয়ে loop-কে নিয়ন্ত্রণ ফেরত দেয়
  4. 🔀 সেই ফাঁকে loop অন্য প্রস্তুত Task-গুলো চালায়
  5. 🔔 টাইমার বাজলে Task আবার ready হয় আর await-এর পরের লাইন থেকে চলে

Python-এ async def f(): return 5 লেখা আছে। একটি সাধারণ স্ক্রিপ্টে সরাসরি x = f() লিখলে x-এ কী থাকে?

  • 5
  • None
  • একটি coroutine object
  • SyntaxError

Python asyncio-তে তিনটি coroutine-এর অপেক্ষা ৩, ১ আর ২ সেকেন্ড। await asyncio.gather(a(), b(), c()) চালালে আনুমানিক কত সময় লাগবে, আর ফলের list কোন ক্রমে আসবে?

  • ৬ সেকেন্ড; a, b, c ক্রমে
  • ৩ সেকেন্ড; a, b, c ক্রমে
  • ৩ সেকেন্ড; b, c, a ক্রমে (যে আগে শেষ)
  • ১ সেকেন্ড; b, c, a ক্রমে

Python-এ একটি async ওয়েব সার্ভারের এক handler-এ time.sleep(2) লেখা আছে। ওই handler-এ একটি অনুরোধ এলে কী ঘটে?

  • শুধু ওই অনুরোধটি ২ সেকেন্ড দেরি করে, বাকিরা স্বাভাবিক চলে
  • Python স্বয়ংক্রিয়ভাবে এটাকে asyncio.sleep-এ বদলে নেয়
  • সাথে সাথে RuntimeError দেয়
  • ২ সেকেন্ডের জন্য event loop জমে যায়, সব ব্যবহারকারীর অনুরোধ থেমে থাকে

Python 3.11-এর asyncio.TaskGroup-এর ভেতরের তিনটি Task-এর একটি ValueError দিলে কী হয়?

  • বাকি Task-গুলো cancel হয়, আর ব্লক থেকে একটি ExceptionGroup ওঠে
  • বাকি Task-গুলো পেছনে চলতে থাকে, আর সাধারণ ValueError আসে
  • ভুলটি উপেক্ষা করে বাকি দুটির ফল দেওয়া হয়
  • ব্যর্থ Task-টি স্বয়ংক্রিয়ভাবে আবার চালানো হয়

Python-এ ছবির আকার বদলানোর মতো CPU-র ভারী কাজ ১০০টি। কাজগুলো দ্রুত শেষ করার সবচেয়ে উপযুক্ত উপায় কোনটি?

  • প্রতিটি কাজ async def বানিয়ে asyncio.gather দিয়ে চালানো
  • সাধারণ CPython-এ asyncio.to_thread দিয়ে প্রতিটি কাজ thread-এ পাঠানো
  • ProcessPoolExecutor দিয়ে একাধিক process-এ ভাগ করা
  • asyncio.Semaphore(100) ব্যবহার করা

Python asyncio-তে একটি SMS গেটওয়েতে একসাথে সর্বোচ্চ ৩টি অনুরোধ পাঠাতে চাইলে কোনটি সঠিক টুল?

  • asyncio.timeout(3)
  • asyncio.Semaphore(3) আর প্রতিটি অনুরোধ async with sem-এর ভেতরে
  • asyncio.gather দিয়ে সব একসাথে
  • প্রতিটি অনুরোধের পর time.sleep(1)

শব্দকোষ

লেখার মধ্যে ডটেড দাগ দেওয়া শব্দে মাউস রাখুন বা চাপুন — অর্থ দেখাবে। এই পর্বের সব নতুন শব্দ এখানে একসাথে:

মনে রাখুন

  1. asyncio = এক thread, অনেক অপেক্ষা। কাজ বদল হয় শুধু await-এ, কাজ নিজে সরে দাঁড়ায় (cooperative). I/O-র জন্য দারুণ, CPU-র হিসাবে কোনো লাভ নেই।
  2. async def ডাকলে coroutine object — body চলে await, Task বা asyncio.run-এ। await ভুললে কাজটি হয়ই না, শুধু “never awaited” সতর্কবার্তা।
  3. পরপর await = যোগফল; gather/TaskGroup = সবচেয়ে লম্বাটি। নতুন কোডে TaskGroup: ব্যর্থ হলে বাকিদের cancel, ভুল ExceptionGroup-এ, except* দিয়ে ধরা।
  4. loop কখনো আটকাবেন না। time.sleep → asyncio.sleep, requests → async client, ব্লক করা কোড → to_thread, ভারী হিসাব → process.
  5. নিয়ন্ত্রণে রাখুন: প্রতিটি বাইরের অনুরোধে timeout, Semaphore দিয়ে concurrency-র সীমা, Queue দিয়ে producer–consumer, আর Task-এর reference কখনো হারাবেন না।

পরের পর্বে: thread, process, asyncio — দ্রুত করার অনেক উপায় দেখলেন। কিন্তু প্রোগ্রাম আসলে কোথায় সময় নষ্ট করছে, তা না জেনে দ্রুত করার চেষ্টা প্রায়ই ভুল জায়গায় শ্রম। পরের পর্বে আগে মাপা, তারপর দ্রুত করা — profiler, timeit আর সঠিক ডেটা স্ট্রাকচার।

আরও পড়ুন · ABCL TECH-এ আপনার পরবর্তী পদক্ষেপ

এই পর্বটি শেয়ার করুন:
এই পর্বে ভুল পেয়েছেন? জানান