AzLearn

أنماط async/await

async/await Patterns

مفهوم ~22 دقيقة

أنماط async/await — async/await Patterns

في الدرس السابق تعلّمت الأساسيات: async def و await وحلقة الأحداث. رأيت كيف تعمل المهام بالتسلسل عند استخدام await واحدة تلو الأخرى. في هذا الدرس ستتعلم كيف تجعلها تعمل بالتوازي وكيف تتعامل مع أنماط أكثر تعقيداً.

asyncio.gather — التوازي الحقيقي

asyncio.gather هي أقوى أداة في asyncio للتشغيل المتوازي. تأخذ عدداً من coroutines وتُشغّلها جميعاً في نفس الوقت، ثم تُعيد نتائجها بنفس الترتيب الذي أُدخلت به — بغض النظر عن أيها انتهت أولاً.

main.go

الوقت الكلي تقريباً هو وقت أطول مهمة واحدة (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 فتتيح إنشاء مهام في أي وقت ومتابعتها بشكل مستقل:

main.go

الفرق الجوهري بين create_task و gather:

  • create_task تبدأ المهمة فوراً في الخلفية، حتى قبل أن تصل لـ await
  • gather تبدأ المهام كلها معاً عند نقطة await
  • create_task تُعيد كائن Task يمكنك إلغاؤه أو الاستعلام عن حالته

async for — التكرار غير المتزامن

async for تتيح التكرار على مصادر بيانات تُنتج قيمها بشكل غير متزامن — مثل بث بيانات من شبكة أو قراءة ملف كبير:

main.go

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موارد تحتاج تنظيفاً غير متزامن (ملفات، اتصالات)
تحدي — Challenge