Python-এ async/await: asyncio সময়সহ ব্যাখ্যা

async/await একটি thread-কে একসঙ্গে অনেক ধীর কাজের অপেক্ষায় রাখে। আসল সময়সহ gather, semaphore, timeout ও blocking call-এর ভুল দেখুন।

সমান্তরাল কাজের লেনে অপেক্ষার সময় একে অন্যের সঙ্গে মিলছে

বেশিরভাগ প্রোগ্রাম সময় কাটায় অপেক্ষায়: web request, database, ফাইলের জন্য। async/await একটি thread-কে অপেক্ষা শুরু করে ইতিমধ্যে অন্য কাজে যেতে দেয়, ফলে অনেক অপেক্ষা একসঙ্গে মেলে। এটি CPU-ভারী code দ্রুত করে না, আর কয়েকটি core-এ সমান্তরালে code চালায় না। নিচের code ও সময় প্রকাশের আগে Python 3-এ চালানো (sleep network call-এর বদলে)।

মূল ধারণা

  • async def একটি coroutine বানায়। এটি ডাকলে চলে না; await করতে হয় বা event loop-কে দিতে হয়।
  • await বলে "এটি শেষ না হওয়া পর্যন্ত আমাকে থামাও, অন্যদের চলতে দাও"।
  • asyncio.run(main()) event loop শুরু করে।
  • asyncio.gather(...) কয়েকটি awaitable একসঙ্গে চালিয়ে ফল ক্রমে জড়ো করে।

পরপর বনাম একসঙ্গে, semaphore ও timeout

import asyncio, time

async def fetch(name, delay):
    await asyncio.sleep(delay)          # stands in for a network call
    return f"{name} done"

async def main():
    t = time.perf_counter()
    for n in ("a", "b", "c"):
        await fetch(n, 0.3)             # one after another
    print(f"one after another: {time.perf_counter() - t:.1f} s")

    t = time.perf_counter()
    results = await asyncio.gather(fetch("a", 0.3), fetch("b", 0.3), fetch("c", 0.3))
    print(f"gather: {time.perf_counter() - t:.1f} s ->", results)

    sem = asyncio.Semaphore(2)          # at most two at once
    active, peak = 0, 0
    async def limited(i):
        nonlocal active, peak
        async with sem:
            active += 1; peak = max(peak, active)
            await asyncio.sleep(0.1)
            active -= 1
    await asyncio.gather(*(limited(i) for i in range(6)))
    print("semaphore(2): at most", peak, "running at once")

    try:
        await asyncio.wait_for(fetch("slow", 1.0), timeout=0.2)
    except asyncio.TimeoutError:
        print("timeout: gave up after 0.2 s")

asyncio.run(main())
one after another: 0.9 s
gather: 0.3 s -> ['a done', 'b done', 'c done']
semaphore(2): at most 2 running at once
timeout: gave up after 0.2 s

0.3 সেকেন্ডের তিনটি অপেক্ষা পরপর 0.9 সেকেন্ড আর gather-এ 0.3 সেকেন্ড নিয়েছে। semaphore একসঙ্গে চলা কাজ ২টিতে ধরে রেখেছে, এভাবে শতশত request দিয়ে একটি সেবার ওপর চাপ এড়ানো যায়। wait_for ১ সেকেন্ড লাগা কাজের জন্য ০.২ সেকেন্ড পর অপেক্ষা ছেড়েছে।

যে ভুলে সব জমে যায়

সবাই একটি thread ভাগ করে, তাই একটি blocking call সব কাজ থামায়।

async def slow_block():
    time.sleep(0.5)          # a blocking call inside async code freezes everything

ticks = []
async def ticker():
    for _ in range(3):
        ticks.append(round(time.perf_counter() - t0, 1)); await asyncio.sleep(0.1)

t0 = time.perf_counter()
await asyncio.gather(ticker(), slow_block())
print("ticker ticks while blocked by time.sleep:", ticks)
ticker ticks while blocked by time.sleep: [0.0, 0.5, 0.6]

ticker-এর প্রতি 0.1 সেকেন্ডে টিক করার কথা। তার বদলে 0.0-তে টিক করে 0.5 পর্যন্ত অপেক্ষা করেছে, কারণ time.sleep(0.5) loop আটকে দিয়েছে। এর বদলে await asyncio.sleep ব্যবহার করুন, বা blocking কাজ asyncio.to_thread(...) দিয়ে thread-এ সরান।

কখন asyncio

  • ভালো মানায়: অনেক সমসাময়িক network call, web server, chat, ভদ্রভাবে অনেক পেজ scrape, যা মূলত অপেক্ষা করে।
  • কম মানায়: ভারী গণনা (process ব্যবহার করুন), এক-দুটি call-এর সহজ script, blocking library-নির্ভর code।

সাধারণ ভুল

  • await ছাড়া coroutine ডাকা, যা শুধু একটি object বানায় ও কিছুই চালায় না (Python সতর্ক করে "coroutine was never awaited")।
  • async code-এ blocking library (requests, time.sleep) ব্যবহার।
  • সীমা ছাড়াই হাজার হাজার task একসঙ্গে শুরু। semaphore ব্যবহার করুন।
  • একটি ব্যর্থ task অন্যদের লুকিয়ে ফেলা। gather(..., return_exceptions=True) ত্রুটিকে ফল হিসেবে ফেরায়।
  • timeout ভুলে যাওয়া। timeout ছাড়া request চিরকাল অপেক্ষা করতে পারে।

মূল কথা: যে code অপেক্ষা করে তাতে async/await ব্যবহার করুন, স্বাধীন অপেক্ষা gather দিয়ে চালান, semaphore দিয়ে সমসাময়িকতা সীমিত করুন, timeout দিন, আর loop কখনও আটকাবেন না।


Programming roadmaps সিরিজ, পর্ব ৩ (ভালো Python লেখা): 3 নম্বর (মোট 5)। আগের লেখা: Python-এ regular expression: ফাঁদসহ ব্যবহারিক গাইড.

আরও পড়ুন

Python-এ regular expression: ফাঁদসহ ব্যবহারিক গাইড

Regular expression লেখা খুঁজে নতুন রূপ দেয়। group, greedy বনাম lazy, বাংলাদেশি মোবাইল নম্বর যাচাই ও যে backtracking ফাঁদ server জমিয়ে দিতে পারে তা শিখুন।

প্রোগ্রামিং•২ মিনিটে পড়া