تا اینجا یاد گرفتیم چطور با async و await کار کنیم، تسکها رو مدیریت کنیم، و ورودی و خروجی غیرهمزمان رو هندل کنیم. اما اگه بخوایم برنامهای بسازیم که واقعا در دنیای واقعی قابل نگهداری و توسعه باشه، فقط دونستن این مفاهیم کافی نیست. باید بدونیم چطور ساختار بدیم به برنامههامون تا هم مقیاسپذیر باشن، هم قابل تست و توسعه.
برنامههای async معمولاً پیچیدهتر از برنامههای sync هستن. چون رفتار اونها در زمان اجرا به زمانبندی تسکها و مدیریت منابع مشترک بستگی داره. بنابراین بدون ساختار درست، خیلی راحت ممکنه برنامه تبدیل به چیزی بشه که:
- دیباگ کردنش سخت باشه،
- بخشهای مختلفش به هم وابسته بشن،
- و در نهایت، توسعهدهندههای بعدی ازش فرار کنن
ساختاردهی یعنی تعیین اینکه چه کسی چه کاری انجام بده، و چه کسی کارها رو هماهنگ کنه.
هر تابع async باید فقط یک کار انجام بده، اما اون کار رو بهدرستی انجام بده. یعنی بهجای اینکه یک coroutine بزرگ بنویسیم که هم داده میگیره، هم پردازش میکنه، هم ذخیره میکنه، بهتره وظایف رو تفکیک کنیم:
main()
├── fetcher()
│ ├── fetch_api_1()
│ └── fetch_api_2()
├── processor()
└── saver()
به این ترتیب، اگه بخوایم منبع جدیدی به سیستم اضافه کنیم، فقط یک تابع جدید اضافه میکنیم، بدون اینکه بقیه کدها رو تغییر بدیم.
همونطور که در برنامههای معمولی از معماریهای مثل MVC یا Service Layer استفاده میکنیم، در برنامههای async هم باید چنین ساختاری داشته باشیم.
یک تقسیمبندی استاندارد میتونه این باشه:
- I/O Layer: شامل توابعی که با شبکه یا فایل سر و کار دارن (مثل aiohttp یا aiofiles).
- Logic Layer: شامل توابعی که دادهها رو پردازش میکنن و تصمیمات منطقی میگیرن.
- Orchestration Layer: بخشی که وظیفه داره اجرای تسکها رو هماهنگ کنه، خطاها رو بگیره و ترتیب اجرا رو مشخص کنه.
در این مدل، توابع async پایینی (مثل fetch یا write) از سطح بالا (orchestrator) اطلاعی ندارن — فقط وظیفهی خودشون رو انجام میدن.
در برنامههای async واقعی، معمولاً با منابع مشترک سر و کار داریم — مثل session شبکه، connection دیتابیس یا صف پیامها. استفاده از کلاس به ما کمک میکنه تا این منابع رو به شکل تمیزتری مدیریت کنیم.
import aiohttp
import asyncio
class DataFetcher:
def __init__(self):
self.session = None
async def __aenter__(self):
self.session = aiohttp.ClientSession()
return self
async def __aexit__(self, *args):
await self.session.close()
async def fetch(self, url: str):
async with self.session.get(url) as response:
return await response.text()
async def main():
async with DataFetcher() as fetcher:
html = await fetcher.fetch("https://example.com")
print(html[:200])
asyncio.run(main())در این مثال از context manager غیرهمزمان (__aenter__ و __aexit__) استفاده کردیم تا باز و بسته شدن session بهصورت امن انجام بشه.
وقتی برنامه رشد میکنه، ساختار پوشهای مناسب خیلی حیاتی میشه. ساختاری مثل این میتونه شروع خوبی باشه:
my_async_app/
├── main.py
├── services/
│ ├── fetcher.py
│ ├── processor.py
│ └── saver.py
├── utils/
│ ├── logger.py
│ └── config.py
└── core/
└── orchestrator.py
در اینجا:
- فایلهای داخل
services/شامل وظایف async مجزا هستن.
- core/orchestrator.py مسئول هماهنگی بین اونهاست.
- utils/ شامل ابزارهای کمکی مثل log و پیکربندی برنامهست.
در این مثال، چند سرویس async داریم که دادهها رو از منابع مختلف میگیرن، پردازش میکنن و ذخیره میکنن. همهی این مراحل توسط orchestrator کنترل میشن:
import asyncio
from services.fetcher import fetch_data
from services.processor import process_data
from services.saver import save_data
async def run_pipeline():
try:
data = await fetch_data()
processed = await process_data(data)
await save_data(processed)
except Exception as e:
print(f"Error: {e}")
async def scheduler():
while True:
await run_pipeline()
await asyncio.sleep(60) # اجرا هر دقیقه
asyncio.run(scheduler())نکته مهم اینجاست که orchestrator تنها مسئول هماهنگی بین مراحل مختلفه. خودش کاری انجام نمیده، بلکه تسکها رو زمانبندی و کنترل میکنه.
در برنامههای async، مدیریت خطاها از اهمیت ویژهای برخورداره. مثلاً اگه یکی از coroutineها با خطا مواجه بشه، بقیه coroutineها باید بدونن چه واکنشی نشون بدن.
async def safe_gather(*tasks):
results = await asyncio.gather(*tasks, return_exceptions=True)
for result in results:
if isinstance(result, Exception):
print(f"Handled error: {result}")
return resultsتابع بالا تضمین میکنه که خطا در یکی از تسکها باعث توقف کل برنامه نشه.
- هیچ coroutineی نباید بیش از حد کار کنه. تقسیمش کن به تسکهای کوچیکتر.
- همیشه منابع مثل session یا connection رو مدیریت کن. از context manager استفاده کن.
- استفاده از asyncio.Semaphore یا Queue برای کنترل تعداد تسکهای همزمان میتونه از overload شدن منابع جلوگیری کنه.
- از logging درست استفاده کن. مخصوصاً برای ردگیری جریان تسکها.
- واحد تست (unit test) برای async توابع بنویس تا از رفتار درستشون مطمئن شی.
در طراحی و ساختاردهی برنامههای async، باید ذهنیت مهندسی سیستم داشته باشی:
- نخهای ظریف مثل coroutineها هستن که باید با نظم باهم کار کنن.
- مغز سیستم مثل orchestrator عمل میکنه و وظایف رو زمانبندی میکنه.
- منابع async باید با دقت باز و بسته بشن.
- و همهچیز باید قابل گسترش و تست باشه.