برنامهنویسی ناهمزمان در پایتون را یک بار برای همیشه بفهم — ساده، دقیق، و با کدهایی که خودت میتوانی اجرا کنی
فرض کن رفتهای کافه. سفارش قهوه میدهی. اگر باریستا تا آماده شدن قهوهی تو هیچ کار دیگری نکند و فقط منتظر بماند، صف طولانی میشود. اما باریستای باهوش، وقتی قهوهی تو در حال دمکشیدن است، سفارش نفر بعدی را میگیرد. این دقیقاً همان کاری است که async و await در پایتون انجام میدهند: وقتی برنامه منتظر چیزی است (مثل پاسخ یک سرور)، بهجای بیکار ماندن، کار دیگری انجام میدهد.
این مقاله را طوری نوشتهایم که اگر تازهکاری، مفهوم را کامل بگیری؛ و اگر حرفهای هستی، دقیقترین و مدرنترین روشها (مثل TaskGroup در پایتون ۳.۱۱+) را ببینی. همهی کدها قابل کپی و اجرا هستند — کافی است پایتون ۳.۱۱ یا بالاتر داشته باشی.
async برای کارهای «I/O-bound» عالی است: درخواست شبکه، خواندن از دیتابیس، کار با فایل، صدا زدن API. اما برای کارهای «CPU-bound» (محاسبات سنگین مثل پردازش تصویر) مناسب نیست — برای آنها باید سراغ multiprocessing بروی. این مهمترین نکتهای است که باید از همین اول بدانی.
برنامهی معمولی (synchronous) خطبهخط اجرا میشود: تا یک خط تمام نشود، خط بعدی شروع نمیشود. اگر خطی منتظر دانلود یک فایل باشد، کل برنامه همانجا متوقف میماند تا دانلود تمام شود.
برنامهی ناهمزمان (asynchronous) فرق دارد: وقتی به یک نقطهی «انتظار» میرسد، کنترل را به کارهای دیگر میدهد و وقتی انتظار تمام شد، برمیگردد. توجه کن: این یعنی «همزمانی» (concurrency) نه «موازیسازی» (parallelism). همه چیز در یک thread اجرا میشود — فقط هوشمندانه بین کارها جابهجا میشویم.
هر تابعی که با async def تعریف شود، یک «کوروتین» است. نکتهی کلیدی که خیلیها را گیج میکند: وقتی یک کوروتین را صدا میزنی، اجرا نمیشود! فقط یک «شیء کوروتین» ساخته میشود. برای اجرای واقعی باید آن را await کنی یا با asyncio.run() اجرا کنی.
اگر یک کوروتین را بدون await صدا بزنی (مثلاً فقط make_coffee())، پایتون هشدار "coroutine was never awaited" میدهد و کد داخلش اصلاً اجرا نمیشود. این رایجترین اشتباه تازهکارهاست. قانون طلایی: هر کوروتین یا باید await شود یا داخل asyncio.run() اجرا شود.
تا اینجا فقط یک کار را await کردیم. قدرت async وقتی معلوم میشود که چند کار را همزمان اجرا کنیم. اول بیایید ببینیم اگر کارها را پشتسرهم (sequential) اجرا کنیم چه میشود:
سه ثانیه طول کشید چون کار دوم تا تمام شدن کار اول صبر کرد. حالا بیایید همان دو کار را «همزمان» اجرا کنیم تا تفاوت را ببینی. از پایتون ۳.۱۱ به بعد، بهترین و امنترین راه، استفاده از asyncio.TaskGroup است که «همزمانی ساختارمند» را پیاده میکند.
چرا ۲ ثانیه و نه ۳ ثانیه؟ چون هر دو کار در لحظهی صفر شروع شدند. کار اول بعد از ۱ ثانیه و کار دوم بعد از ۲ ثانیه (که همزمان شمرده شد) تمام شد. این همان صرفهجویی در زمان است که async به ما میدهد — مخصوصاً وقتی دهها درخواست شبکه را همزمان بفرستی.
وقتی با TaskGroup کار میکنی، برای گرفتن نتیجهی هر Task باید بعد از بسته شدن بلاک with، متد result() را صدا بزنی. این یک نکتهی مهم است که حتی حرفهایها گاهی فراموش میکنند.
شاید در کدهای قدیمیتر زیاد asyncio.gather() را دیده باشی. این روش هنوز کار میکند و برای کارهای ساده خوب است، اما یک تفاوت مهم با TaskGroup دارد که باید بدانی.
تفاوت کلیدی: اگر یکی از کارها در gather خطا بدهد، بقیهی کارها کنسل نمیشوند و خطا ممکن است گم شود (مگر اینکه return_exceptions=True بگذاری و خودت نتیجهها را چک کنی). اما در TaskGroup، اگر یک کار خطا بدهد، بقیه خودکار کنسل میشوند و خطاها در قالب ExceptionGroup بالا میآیند. برای همین در پایتون ۳.۱۱+ توصیه میشود برای کارهای مرتبط، TaskGroup را ترجیح بدهی.
| ویژگی | asyncio.gather | asyncio.TaskGroup |
|---|---|---|
| نسخه پایتون | همه نسخهها | ۳.۱۱ به بعد |
| اگر یک کار خطا دهد | بقیه ادامه میدهند | بقیه کنسل میشوند |
| مدیریت خطا | دستی (return_exceptions) | خودکار (ExceptionGroup) |
| گرفتن نتیجه | لیست مستقیم | با task.result() |
| توصیهی امروز | کارهای ساده | روش پیشفرض |
گاهی نمیخواهی بینهایت منتظر یک کار بمانی. مثلاً اگر یک API بعد از ۳ ثانیه جواب نداد، میخواهی بیخیال شوی. برای این کار asyncio.wait_for() عالی است:
async در دنیای واقعی همهجا هست. فریمورک FastAPI کاملاً بر پایهی async ساخته شده. کتابخانههایی مثل httpx و aiohttp برای درخواستهای شبکهی همزمان از آن استفاده میکنند. حتی وقتی با صف پیام (مثل Redis یا Kafka) کار میکنی، الگوهای async بهشدت پرکاربردند.
اگر میخواهی ببینی async در یک فریمورک واقعی چطور به کار میرود، مقالههای «جنگو ۶» و «Redis، RabbitMQ یا Kafka؟» در همین سایت نقطهی شروع خوبی هستند — مخصوصاً برای دیدن اینکه کارهای I/O-bound در عمل چطور مدیریت میشوند.
حالا مفهوم async و await را کامل میدانی — هم تئوری، هم کد اجراشدنی. بهترین راه برای تثبیت یادگیری این است که همین کدها را کپی کنی، اجرا کنی، و کمی تغییرشان بدهی. مثلاً delayها را عوض کن و ببین خروجی چطور تغییر میکند. موفق باشی!