asyncio و پایتون نخ‌آزاد

asyncio از یک حلقه رویداد به‌عنوان زمان‌بند استفاده می‌کند تا با جابه‌جایی بین وظایف و امکان‌پذیر ساختن عملیات ورودی/خروجی غیرمسدودکننده، هم‌روندی بسیار کارآمدی را فراهم کند. این امر باعث عملکرد بهتر در موارد استفاده محدود به ورودی/خروجی می‌شود. همچنین امکان واگذاری کارهای محدود به CPU به یک استخر نخ یا فرایند را فراهم می‌کند، اما این کار در CPython همچنان محدود به قفل سراسری مفسر است.

با این حال، در free-threaded Python، GIL غیرفعال است و پایتون می‌تواند کد چندنخی واقعی را اجرا کند. این بدان معناست که asyncio اکنون می‌تواند از چندین هسته CPU بدون محدودیت‌های اعمال‌شده توسط GIL بهره ببرد.

از پایتون ۳.۱۴، asyncio پشتیبانی درجه‌یک از پایتون نخ‌آزاد دارد و پیاده‌سازی asyncio برای استفاده در محیط چندنخی ایمن است.

یک حلقه رویداد واحد روی یک هسته می‌تواند اتصال‌های زیادی را به‌طور همروند مدیریت کند، اما کد پایتونی که برای مدیریت هر یک اجرا می‌شود همچنان به‌صورت متوالی اجرا می‌شود. هنگامی که درخواست‌ها شامل مقدار قابل‌توجهی محاسبه به‌ازای هر درخواست شوند، مدیریت آن‌ها به گلوگاه تبدیل می‌شود و یک هسته دیگر نمی‌تواند پاسخگو باشد. ترکیب asyncio با نخ‌ها در اینجا بیشترین کاربرد را دارد: با اجرای یک حلقه رویداد به‌ازای هر نخ، مدیریت درخواست‌های مختلف می‌تواند به‌صورت موازی روی چندین هسته CPU اجرا شود. همچنین زمانی که نیاز دارید کد مسدودکننده یا محدود به CPU را از یک برنامه asyncio اجرا کنید، مفید است.

همچنین ملاحظه نمائید

Scaling asyncio on Free-Threaded Python، یک پست وبگزارشی از Kumar Aditya که تغییرات داخلی‌ای را توضیح می‌دهد که باعث ایمن و کارآمد شدن asyncio در پایتون نخ‌آزادی (Free-Threaded Python) می‌شوند، به‌همراه بنچمارک‌هایی از بهبودهای حاصل.

ملاحظات ایمنی نخ‌ها

هرچند asyncio به‌گونه‌ای طراحی شده است که در یک محیط پایتون نخ‌آزاد (free-threaded) امن از نظر نخ‌ها (thread-safe) باشد، اما همچنان هنگام استفاده از asyncio با نخ‌ها ملاحظاتی وجود دارد که باید آن‌ها را در نظر داشته باشید:

  1. حلقه رویداد: هر نخ باید حلقه رویداد مخصوص به خود را داشته باشد که نباید بین نخ‌ها به اشتراک گذاشته شود. این امر تضمین می‌کند که حلقه رویداد می‌تواند وظایف و کال‌بک‌های خود را بدون تداخل از سوی نخ‌های دیگر مدیریت کند.

  2. مدیریت وظیفه: وظایف و آینده‌نماها (futures) ایجادشده در یک نخ نباید از نخی دیگر await شوند یا دستکاری شوند.

  3. APIهای ایمن برای نخ: هنگام تعامل با asyncio از چندین نخ، مهم است که از APIهای ایمن برای نخ ارائه‌شده توسط asyncio استفاده کنید، مانند asyncio.run_coroutine_threadsafe() برای ارسال هم‌روال‌ها به یک حلقه رویداد از یک نخ دیگر. اگر نیاز دارید یک کال‌بک را از یک نخ متفاوت فراخوانی کنید، می‌توانید از loop.call_soon_threadsafe() برای زمان‌بندی ایمن آن استفاده کنید.

  4. همگام‌سازی: ابزارهای همگام‌سازی ارائه‌شده توسط asyncio (مانند asyncio.Lock و asyncio.Event) برای استفاده در میان نخ‌ها طراحی نشده‌اند. اگر نیاز به همگام‌سازی بین نخ‌ها دارید، باید به‌جای آن از ابزارهای همگام‌سازی ماژول threading استفاده کنید.

استفاده از asyncio با نخ‌ها

asyncio از اجرای یک حلقه رویداد به ازای هر نخ پشتیبانی می‌کند، که به شما امکان می‌دهد در یک محیط پایتون نخ‌آزادی از چندین هسته CPU بهره ببرید. هر نخ می‌تواند حلقه رویداد خود را اجرا کند، و وظایف می‌توانند به‌طور مستقل روی آن حلقه‌ها زمان‌بندی شوند.

در اینجا مثالی از نحوه‌ی استفاده از asyncio با نخ‌ها آمده است:

import asyncio
import threading

async def worker(name: str) -> None:
    print(f"Worker {name} starting")
    await asyncio.sleep(1)
    print(f"Worker {name} done")

def run_loop(name: str) -> None:
    asyncio.run(worker(name))

threads = [
    threading.Thread(target=run_loop, args=(f"T{i}",))
    for i in range(4)
]
for t in threads:
    t.start()
for t in threads:
    t.join()

در این مثال، هر نخ حلقه رویداد خود را با asyncio.run() ایجاد می‌کند و یک هم‌روال را روی آن اجرا می‌کند. نخ‌ها به‌طور همزمان اجرا می‌شوند و در یک ساخت نخ‌آزاد (free-threaded build) می‌توانند روی هسته‌های جداگانه‌ی CPU به‌صورت موازی اجرا شوند.

تولیدکننده/مصرف‌کننده بین نخ‌ها

هنگامی که یک نخ معمولی (غیر asyncio) نیاز دارد کاری را به یک حلقه‌ی رویداد asyncio که در نخ دیگری اجرا می‌شود بسپارد، از یک سازوکار اولیه‌ی ایمن برای نخ مانند queue.Queue استفاده کنید، نه از asyncio.Queue، که فقط درون یک حلقه‌ی رویداد واحد ایمن است.:

import asyncio
import queue
import threading

def producer(q: queue.Queue[int]) -> None:
    for i in range(5):
        print(f"Producing {i}")
        q.put(i)
    q.shutdown()

async def consumer(q: queue.Queue[int]) -> None:
    while True:
        try:
            item = q.get_nowait()
        except queue.Empty:
            await asyncio.sleep(0.1)
            continue
        except queue.ShutDown:
            break
        print(f"Consumed {item}")
        await asyncio.sleep(item)

q: queue.Queue[int] = queue.Queue()
consumer_thread = threading.Thread(
    target=lambda: asyncio.run(consumer(q))
)
consumer_thread.start()
producer(q)
consumer_thread.join()

تولیدکننده روی نخ اصلی اجرا می‌شود، در حالی که مصرف‌کننده درون یک حلقه رویداد روی نخ اختصاصی خود اجرا می‌شود، با این حال آن‌ها به‌صورت امن از طریق queue.Queue با یکدیگر ارتباط برقرار می‌کنند. وقتی صف خالی است، مصرف‌کننده برای مدت کوتاهی متوقف میشود و دوباره تلاش می‌کند. هنگامی که تولیدکننده کارش را تمام کرد، shutdown() را فراخوانی می‌کند، که باعث می‌شود فراخوانی‌های بعدی get_nowait() استثنا queue.ShutDown را پرتاب کنند تا مصرف‌کننده بتواند به‌صورت تمیز خارج شود.