أنماط async/await
async/await Patterns
أنماط async/await — async/await Patterns
في الدرس السابق تعلّمت الأساسيات: async def و await وحلقة الأحداث. رأيت كيف تعمل المهام بالتسلسل عند استخدام await واحدة تلو الأخرى. في هذا الدرس ستتعلم كيف تجعلها تعمل بالتوازي وكيف تتعامل مع أنماط أكثر تعقيداً.
asyncio.gather — التوازي الحقيقي
asyncio.gather هي أقوى أداة في asyncio للتشغيل المتوازي. تأخذ عدداً من coroutines وتُشغّلها جميعاً في نفس الوقت، ثم تُعيد نتائجها بنفس الترتيب الذي أُدخلت به — بغض النظر عن أيها انتهت أولاً.
الوقت الكلي تقريباً هو وقت أطول مهمة واحدة (0.4s)، وليس مجموع الأوقات (0.9s). هذه هي قوة asyncio.
asyncio.gather مع معالجة الأخطاء
بشكل افتراضي، إذا فشلت إحدى coroutines في gather، فإن gather يُلغي الباقية ويُعيد الاستثناء. يمكن تغيير هذا السلوك:
async def main():
# return_exceptions=True يُعيد الاستثناءات كنتائج بدل رميها
# return_exceptions=True returns exceptions as results instead of raising
results = await asyncio.gather(
fetch_orders(),
failing_task(), # ستفشل هذه
fetch_stats(),
return_exceptions=True
)
for r in results:
if isinstance(r, Exception):
print(f"خطأ: {r}")
else:
print(f"نتيجة: {r}")
asyncio.create_task — تحكم أكبر
asyncio.gather مناسبة عندما تعرف المهام مسبقاً. أما create_task فتتيح إنشاء مهام في أي وقت ومتابعتها بشكل مستقل:
الفرق الجوهري بين create_task و gather:
create_taskتبدأ المهمة فوراً في الخلفية، حتى قبل أن تصل لـawaitgatherتبدأ المهام كلها معاً عند نقطةawaitcreate_taskتُعيد كائنTaskيمكنك إلغاؤه أو الاستعلام عن حالته
async for — التكرار غير المتزامن
async for تتيح التكرار على مصادر بيانات تُنتج قيمها بشكل غير متزامن — مثل بث بيانات من شبكة أو قراءة ملف كبير:
async for تعمل مع أي كائن ينفّذ بروتوكول __aiter__ / __anext__ — تجده في مكتبات قواعد البيانات غير المتزامنة، والـ WebSockets، والبث المباشر.
async with — إدارة السياق غير المتزامنة
async with مثل with العادية لكنها تتيح تنفيذ عمليات غير متزامنة عند الدخول والخروج من السياق:
import asyncio
import aiofiles # مكتبة خارجية لقراءة الملفات بشكل غير متزامن
async def read_file():
# async with يضمن إغلاق الملف حتى عند الأخطاء
# async with ensures file closes even on errors
async with aiofiles.open("data.txt", "r") as f:
content = await f.read()
return content
# مثال آخر: اتصال بقاعدة بيانات
async def db_operation(pool):
async with pool.acquire() as connection:
result = await connection.fetch("SELECT * FROM users")
return result
المبدأ نفسه: الكائن الذي تستخدمه مع async with ينفّذ __aenter__ و __aexit__ غير المتزامنتين.
أنماط متقدمة: timeout والإلغاء
import asyncio
async def slow_operation():
await asyncio.sleep(10) # عملية بطيئة جداً
async def main():
try:
# انتظر بحد أقصى 2 ثانية — Wait maximum 2 seconds
result = await asyncio.wait_for(slow_operation(), timeout=2.0)
except asyncio.TimeoutError:
print("انتهى الوقت المحدد!")
asyncio.wait_for تُلغي المهمة تلقائياً بعد انتهاء المهلة وترمي asyncio.TimeoutError.
الأخطاء الشائعة في asyncio
الخطأ الأول: coroutine غير مُنتظرة
async def main():
# ❌ ينشئ coroutine ولا ينفّذها
fetch_data() # RuntimeWarning: coroutine 'fetch_data' was never awaited
# ✅ صواب
await fetch_data()
الخطأ الثاني: استدعاء دالة blocking داخل async
import requests # مكتبة HTTP تقليدية (blocking)
async def bad():
# ❌ يُجمّد حلقة الأحداث كلها
response = requests.get("https://api.example.com")
async def good():
# ✅ استخدم مكتبة async
# import aiohttp
# async with aiohttp.ClientSession() as session:
# async with session.get("https://api.example.com") as response:
# data = await response.json()
pass
الخطأ الثالث: إنشاء tasks ثم عدم انتظارها
async def main():
# ❌ المهام تُنشأ لكن لا تُكتمل بالضرورة قبل نهاية main
asyncio.create_task(background_job())
# ✅ احتفظ بمرجع وانتظر
task = asyncio.create_task(background_job())
await task
مقارنة: gather مقابل create_task مقابل await مباشر
| الأسلوب | متى تستخدمه |
|---|---|
await f() | مهمة واحدة تحتاج نتيجتها فوراً |
asyncio.gather(f1(), f2()) | مهام متعددة معروفة مسبقاً، تريد نتائجها كلها |
asyncio.create_task(f()) | مهمة تريد بدءها الآن واسترداد نتيجتها لاحقاً |
async for | بث بيانات من مصدر يُنتجها تدريجياً |
async with | موارد تحتاج تنظيفاً غير متزامن (ملفات، اتصالات) |