أحدث المقالات
JOURNAL INDEX
لغات البرمجة/2026.10.25/47 VIEWS/5 MIN READ

البرمجة غير المتزامنة في Python: دليل عملي لـ asyncio

عبدالرحمن ربيع علي
عبدالرحمن ربيع علي Software Engineer & AI Builder

دليل عملي لفهم asyncio وEvent Loop والمهام والفرق بين التزامن والتوازي، مع أمثلة على المهلات الزمنية وإلغاء المهام ومعالجة الأخطاء.

البرمجة غير المتزامنة في Python: دليل عملي لـ asyncio

عندما يرسل تطبيقك عشرات الطلبات إلى خدمات خارجية، أو ينتظر ردود API وقراءة الملفات وقواعد البيانات، فإن تنفيذ كل عملية واحدة بعد الأخرى قد يجعل البرنامج بطيئًا حتى لو كان معظم الوقت يُقضى في الانتظار. هنا تظهر فائدة البرمجة غير المتزامنة في Python، لكن استخدام async وawait دون فهم ما يحدث خلفهما قد ينتج كودًا معقدًا أو تطبيقًا يتوقف عن الاستجابة.

يشرح هذا الدليل كيف يعمل asyncio، وما دور حلقة الأحداث (Event Loop) والـ coroutines والمهام، وكيفية اختيار التزامن غير المتزامن أو الخيوط أو العمليات، ثم بناء تدفق عمل يتحمل المهلات الزمنية والإلغاء والأخطاء. الأمثلة تعليمية ويمكن تشغيلها باستخدام Python 3.11 أو أحدث، مع مراجعة التوثيق الرسمي عند اختلاف إصدار بيئتك.

الخلاصة السريعة: استخدم asyncio عندما تقضي العمليات وقتًا كبيرًا في انتظار I/O وتتوفر مكتبات غير متزامنة مناسبة. لا تتوقع منه تسريع حسابات CPU الثقيلة تلقائيًا، ولا تخلط بين إنشاء coroutine وتشغيلها أو بين التزامن والتوازي.

كيف تعمل حلقة الأحداث والمهام؟

مخطط يوضح حلقة الأحداث وطابور ردود النداء وتبادل العمل مع واجهات الشبكة
رسم مفاهيمي لدورة الأحداث؛ تختلف تفاصيل التنفيذ بين اللغات. · الصورة: Byteslovesbits — commons.wikimedia.org (CC BY-SA 4.0)

تبدأ الفكرة من coroutine: دالة تُعرّف باستخدام async def ويمكنها تعليق تنفيذها مؤقتًا عند الوصول إلى await. استدعاء هذه الدالة لا ينفذ جسمها مباشرة بالطريقة المعتادة؛ بل يعيد كائن coroutine. لتشغيلها، يمكنك انتظارها من coroutine أخرى أو جدولة تنفيذها كـ Task داخل حلقة الأحداث.

حلقة الأحداث (Event Loop) هي المجدول الذي يتابع المهام الجاهزة، وينتظر إشعارات عمليات الإدخال والإخراج، ثم يستأنف coroutine عندما تصبح العملية التي تنتظرها جاهزة. في الاستخدام المعتاد، تنفذ الحلقة جزءًا من مهمة واحدة في كل مرة على خيط التنفيذ نفسه. عندما تصل المهمة إلى await على عملية غير متزامنة حقيقية، يمكنها التوقف وإفساح المجال لمهمة أخرى بدل احتلال الخيط أثناء الانتظار.

هذا يعني أن التبديل تعاوني وليس سحرًا يحدث عند كل سطر. إذا استدعيت دالة متزامنة تحجب الخيط، أو نفذت حلقة حسابية طويلة داخل coroutine، فلن تتمكن الحلقة من تشغيل المهام الأخرى حتى ينتهي ذلك الجزء. وجود كلمة async لا يحوّل أي عملية تلقائيًا إلى عملية غير حاجبة.

يُستخدم asyncio.run() عادة كنقطة دخول لتطبيق صغير أو سكربت، حيث ينشئ حلقة أحداث ويدير تشغيل coroutine الرئيسية ثم يغلق الحلقة. داخل تطبيق يعمل أصلًا بحلقة أحداث، مثل بعض خوادم الويب أو بيئات الدفاتر التفاعلية، لا تستدعِ asyncio.run() عشوائيًا؛ استخدم آلية التشغيل التي توفرها البيئة.

Coroutine ليست Task

الـ coroutine تمثل العمل غير المتزامن، أما Task فهي غلاف يدير جدولة coroutine ويتيح متابعة نتيجتها أو إلغائها. إنشاء Task يسمح ببدء عدة أعمال قابلة للتقدم بالتداخل، لكن لا يعني أن جميعها تعمل في اللحظة نفسها على عدة أنوية. استخدم asyncio.create_task() عندما تحتاج إلى تشغيل عمل في الخلفية بالتزامن مع استمرار coroutine الحالية، واحفظ مرجعًا واضحًا للمهام التي تحتاج إلى إدارتها.

للتعمق في طريقة انتقال البرنامج من المصدر إلى التنفيذ، راجع دليل المترجمات والمفسرات وJIT؛ فهو يشرح طبقة مختلفة تكمل فهمك لكيفية تشغيل الكود، لا كيفية جدولة عمليات I/O.

مثال عملي: تشغيل عدة عمليات I/O معًا

لنفترض أنك تريد جلب معلومات من ثلاث خدمات مستقلة. التنفيذ المتسلسل ينتظر انتهاء الطلب الأول قبل بدء الثاني، بينما يمكن للنسخة غير المتزامنة بدء الطلبات ثم انتظار النتائج. يوضح المثال التالي شكل التنظيم، مع استخدام تأخير غير متزامن بدل الاتصال بخدمة حقيقية حتى يبقى المثال قابلًا للتشغيل دون مفاتيح API أو اعتماد خارجي.

import asyncio

async def fetch_item(name: str, delay: float) -> str:
    print(f"starting {name}")
    await asyncio.sleep(delay)  # simulate waiting for I/O
    return f"{name}: ready"

async def main() -> None:
    tasks = [
        asyncio.create_task(fetch_item("users", 1.0)),
        asyncio.create_task(fetch_item("orders", 0.5)),
        asyncio.create_task(fetch_item("inventory", 0.8)),
    ]

    results = await asyncio.gather(*tasks)
    for result in results:
        print(result)

if __name__ == "__main__":
    asyncio.run(main())

كل استدعاء إلى fetch_item ينتج coroutine، ثم تحولها create_task إلى مهمة مجدولة. عند الوصول إلى asyncio.sleep تتوقف المهمة مؤقتًا دون حجب الخيط، ما يتيح للحلقة تشغيل المهمتين الأخريين. تجمع asyncio.gather النتائج وتعيدها بترتيب المدخلات، وليس بالضرورة بترتيب انتهاء المهام.

في تطبيق حقيقي، استبدل التأخير بعميل HTTP يدعم async، مثل عميل موثق يدعم واجهة غير متزامنة. لا تستدعِ عميل HTTP متزامنًا داخل coroutine وتتوقع أن يصبح غير حاجب. كذلك، المثال لا يضع حدًا لعدد المهام؛ إنشاء آلاف المهام دفعة واحدة قد يضغط الذاكرة والخدمة المستهدفة. استخدم semaphore أو طابورًا محدود السعة عندما تحتاج إلى تحديد عدد العمليات الجارية.

لماذا لا نستخدم gather لكل شيء؟

التجميع مناسب عندما تكون الأعمال مستقلة وتحتاج إلى نتائجها كلها. لكنه لا يضع سياسة عمل كاملة وحده: يجب أن تحدد ما يحدث إذا فشلت مهمة، وما إذا كان من المقبول متابعة المهام الأخرى، وكيف ستتعامل مع الإلغاء. في Python 3.11 وما بعده، يمكن أن يكون asyncio.TaskGroup اختيارًا أوضح لإدارة مجموعة مترابطة من المهام؛ إذ يوفر نطاقًا لإدارة دورة حياتها والتعامل مع الأخطاء. اختر الأداة وفق سلوك الفشل الذي تريده، لا وفق قصر الكود فقط.

التزامن أم التوازي: كيف تختار؟

مخطط يوضح العلاقة بين البرنامج والعملية والخيوط وجدولة المعالج
مفاهيم العملية والخيوط وجدولة المعالج؛ يوضح الرسم نموذج التنفيذ على مستوى نظام التشغيل. · الصورة: Hooman Mallahzadeh — commons.wikimedia.org (CC BY-SA 4.0)

التزامن (Concurrency) يعني تنظيم عدة أعمال بحيث يتقدم كل منها عندما تتاح له فرصة، وقد تتداخل فترات انتظارها. أما التوازي (Parallelism) فيعني تنفيذ أعمال في الوقت نفسه فعليًا، غالبًا على أنوية أو معالجات متعددة. تستطيع asyncio إدارة عدد كبير من عمليات I/O المتداخلة بكفاءة، لكنها لا تجعل مهمة حسابية ثقيلة تعمل تلقائيًا على نواة أخرى.

قبل اختيار الأداة، صنّف عنق الزجاجة. إذا كان البرنامج ينتظر الشبكة أو خدمة خارجية، فغالبًا يناسبه async عندما تتوفر مكتبة غير حاجبة. إذا كانت لديك مكتبة متزامنة لا يمكن استبدالها بسهولة، فقد يكون تشغيلها عبر asyncio.to_thread() حلًا عمليًا كي لا تحجب حلقة الأحداث. أما الأعمال الحسابية الثقيلة، فقد تحتاج إلى ProcessPoolExecutor أو عملية منفصلة، مع قياس تكلفة نقل البيانات وإنشاء العمليات.

الحالةالاختيار الأوليملاحظة
طلبات HTTP كثيرةasyncio مع عميل asyncأضف حدودًا للطلبات وإعادة المحاولة المنضبطة
مكتبة متزامنة حاجبةasyncio.to_threadلا تحل كل أنواع حمل CPU بهذه الطريقة
ضغط حسابي ثقيلعمليات منفصلة أو ProcessPoolقِس كلفة التسلسل والذاكرة
مهمة بسيطة متزامنةكود عاديلا تضف تعقيدًا بلا حاجة

لا يوجد فائز واحد لكل مشروع. عدد الاتصالات، ومعدل الأخطاء، وحدود الخدمة الخارجية، ومتطلبات الاستجابة، وخبرة الفريق كلها تؤثر في القرار. اقرأ أيضًا دليل الملكية والاستعارة في Rust إذا كان تصميمك يتضمن مكونات عالية الأداء أو حساسة لإدارة الذاكرة؛ فاختيار نموذج التنفيذ منفصل عن اختيار ضمانات الذاكرة.

روابط ذات صلة

المهل الزمنية والإلغاء والتنظيف

رسم أصلي يوضح تسلسل تحديد المهلة وإلغاء المهمة ثم تنظيف الموارد
مخطط أصلي يوضح دورة التعامل مع المهلات والإلغاء والتنظيف.

أي عملية تنتظر شبكة أو خدمة خارجية قد تتأخر أكثر من المتوقع. لذلك لا يكفي أن يكون الكود غير متزامن؛ يجب أن تحدد مدة الانتظار المقبولة وما الذي يحدث عند تجاوزها. في Python 3.11+ يتيح asyncio.timeout() وضع نطاق زمني واضح حول عملية غير متزامنة. كما يمكن استخدام asyncio.wait_for() لفرض مهلة على انتظار coroutine أو awaitable محدد.

import asyncio

async def load_profile():
    await asyncio.sleep(5)  # replace with an async request
    return {"name": "Example"}

async def main():
    try:
        async with asyncio.timeout(2):
            profile = await load_profile()
            print(profile)
    except TimeoutError:
        print("The operation exceeded its deadline")

asyncio.run(main())

المثال يحاكي عملية تستغرق خمس ثوانٍ مع مهلة ثانيتين؛ لذلك ستُرفع TimeoutError بدل انتظار انتهاء التأخير. في تطبيق إنتاجي، ينبغي أن تكون المهلة مناسبة لطبيعة الخدمة، وأن تتفق مع المهلات في طبقات HTTP والوكيل العكسي وقاعدة البيانات حتى لا تتراكم عمليات معلقة بلا داعٍ.

الإلغاء جزء من إدارة الموارد، وليس مجرد زر إيقاف. عند إلغاء Task، تتلقى coroutine استثناء CancelledError عند نقطة مناسبة. ينبغي عادة السماح بانتشار الإلغاء بعد تنفيذ التنظيف، وعدم ابتلاع الاستثناء بصمت؛ لأن ذلك قد يجعل النظام يظن أن العمل اكتمل بينما هو في الحقيقة توقف بطريقة غير متوقعة.

استخدم try/finally لإغلاق الملفات أو تحرير الأقفال أو إنهاء جلسة عميل عند الحاجة. إذا كان المورد يدعم مدير سياق، ففضّله لأنه يجعل دورة الحياة أوضح. لا تضع أعمال تنظيف طويلة لا تنتهي في مسار الإلغاء؛ اجعلها محدودة، وتعامل مع أي مورد خارجي وفق بروتوكول الإغلاق الذي توثقه مكتبته.

معالجة الأخطاء واختبار الكود غير المتزامن

الأخطاء في البرامج غير المتزامنة لا تظهر دائمًا عند السطر الذي أنشأ المهمة. قد تفشل مهمة بعد أن يكون المستدعي قد تابع عمله، لذلك يجب أن تكون دورة حياة كل Task واضحة: من أنشأها؟ من ينتظر نتيجتها؟ من يسجل الخطأ؟ ومن يلغيها إذا لم تعد مطلوبة؟ المهمة التي تُنشأ ثم يُهمل مرجعها قد تنتج تحذيرات أو أخطاء غير ملاحظة، خصوصًا عند فشلها.

في العمل المتوازي منطقيًا، تجنب الاعتماد على ترتيب التنفيذ الذي لم تضمنه الواجهة. إذا كانت مهمتان تعدلان الحالة نفسها، فقد تظهر أخطاء ترتيب حتى لو كان التنفيذ على خيط واحد، لأن كل مهمة قد تتوقف عند await ثم تستأنف بعد تغير الحالة. اجعل الحالة المشتركة صغيرة، واستخدم asyncio.Lock عندما تحتاج إلى حماية مقطع حرج، لكن لا تمسك القفل أثناء انتظار طويل لخدمة خارجية إذا كان ذلك يمنع بقية المهام بلا داعٍ.

في اختبارات الوحدة، اختبر السلوك بدل افتراض توقيت ثابت. لا تعتمد على أن مهمة ستنتهي خلال عدد محدد من أجزاء الثانية على كل جهاز؛ استخدم واجهات وهمية أو خدمات اختبار، وحدد المهلات، وافحص النتائج والأخطاء والإلغاء. تأكد من أن حالات الفشل تُختبر، لا المسار السعيد فقط. وقد يساعدك دليل اختبار الخصائص على صياغة قواعد عامة تتحقق منها عبر مدخلات متعددة، إلى جانب الاختبارات التقليدية.

أخطاء شائعة ينبغي تجنبها

  • استدعاء دالة حاجبة داخل coroutine ثم التساؤل عن سبب توقف بقية المهام.
  • إنشاء Tasks دون الاحتفاظ بمراجع أو خطة واضحة لجمع نتائجها.
  • ابتلاع CancelledError أو تحويل كل استثناء إلى نتيجة نجاح.
  • إطلاق عدد غير محدود من الاتصالات من دون مراعاة حدود الخدمة.
  • إعادة المحاولة بلا حد أو بلا تأخير تدريجي، ما قد يضاعف الضغط أثناء العطل.

التعامل الصحيح مع الخطأ يبدأ بتحديد ما إذا كان قابلًا لإعادة المحاولة، وما إذا كانت العملية آمنة للتكرار. طلب قراءة يمكن غالبًا تكراره، لكن إرسال دفعة مالية أو إنشاء سجل جديد قد يحتاج إلى مفتاح idempotency أو تحقق من الحالة قبل الإعادة. لا تجعل سياسة retry عامة لا تميز بين أنواع الأخطاء.

قائمة مراجعة قبل استخدام asyncio في الإنتاج

قبل دمج async في خدمة حقيقية، تأكد من أن المكتبات الأساسية تدعم العمليات غير الحاجبة؛ فمزج عميل متزامن مع حلقة أحداث قد يبدد المكسب كله. قِس زمن الاستجابة ومعدل الأخطاء واستهلاك الذاكرة وعدد الاتصالات المفتوحة تحت حمل واقعي، وقارن النتيجة بخط أساس متزامن بسيط. لا تعتمد على الانطباع أو على اختبار صغير لا يشبه حركة المرور الحقيقية.

ضع حدودًا واضحة للاتصالات والمهام، وحدد مهلات على مستوى الطلب والعملية الكاملة، وقرر ما الذي يمكن إلغاؤه بأمان. سجّل سياقًا مفيدًا مثل معرف الطلب واسم العملية ومدة التنفيذ، لكن لا تكتب رموز الوصول أو البيانات الشخصية في السجلات. أضف مقاييس للمهام المتأخرة والمهلات والإلغاءات؛ فعدد المهام الجاري لا يكشف وحده ما إذا كانت الخدمة تتعافى أم تتراكم عليها الأعمال.

احرص على الإغلاق المنظم. عند إيقاف الخدمة، أوقف استقبال أعمال جديدة، ثم انتظر الأعمال التي يجب أن تنتهي ضمن مهلة، وألغِ ما تبقى، وأغلق العملاء والموارد. يختلف التسلسل الدقيق حسب إطار العمل والمكتبات، لذا اتبع آلية دورة الحياة التي توثقها البيئة بدل فرض حلقة أحداث ثانية داخلها.

الأسئلة الشائعة

هل asyncio أسرع دائمًا من الكود المتزامن؟

لا. يضيف async تعقيدًا وقد يكون أبطأ في المهام الصغيرة أو الحسابية. فائدته تظهر عندما تتداخل فترات انتظار كثيرة وتكون المكتبات غير حاجبة.

هل async وawait يعنيان تشغيل عدة أنوية؟

لا. هما يقدمان نموذجًا للتعاون والانتظار داخل تدفق التنفيذ. التوازي الحسابي يحتاج عادة إلى عمليات أو أدوات أخرى، مع مراعاة تكلفة البيانات والتنسيق.

هل يجب تحويل كل دالة إلى async؟

لا. أبقِ الدوال البسيطة والمتزامنة كما هي ما لم يكن هناك سبب واضح. اجعل حدود النظام غير المتزامن واضحة، ولا تضف async إلى كل طبقة لمجرد أن نقطة الدخول غير متزامنة.

الخاتمة

يصبح asyncio مفيدًا عندما تفهمه كطريقة لإدارة الانتظار وتنسيق دورة حياة المهام، لا كزر تسريع عام. ابدأ بمثال صغير، واستخدم مكتبات غير حاجبة فعلًا، وأضف حدودًا ومهلات وإلغاءً وتنظيفًا واضحًا، ثم اختبر النجاح والفشل تحت حمل ممثل. اختر async للشبكة وعمليات I/O عند ملاءمته، واستخدم الخيوط أو العمليات عندما يفرض نوع العمل ذلك. القرار الجيد هو الذي يجعل النظام أسرع وأكثر قابلية للفهم والمراقبة، لا الذي يضيف كلمات مفتاحية أكثر.

للتطبيق العملي، ابدأ بالتوثيق الرسمي لـ asyncio في Python، وتحقق من سلوك كل واجهة بحسب إصدار Python والمكتبة التي تستخدمها.

مقالات ذات صلة

افتح توثيق asyncio الرسمي

SHARE شارك المقال
عبدالرحمن ربيع علي
كتبه

عبدالرحمن ربيع علي

Software Engineer & AI Builder

مطور برمجيات متكامل ومصمم جرافيك مع أكثر من 4 سنوات خبرة في بناء تطبيقات الويب الحديثة باستخدام PHP و JavaScript و HTML و CSS. خلفية قوية في تصميم UI/UX واستخدام متقدم لأدوات الذكاء الاصطناعي لتعزيز كفاءة التطوير والأتمتة واتخاذ القرارات. حاصل على ماجستير تنفي...

وسوم المقال

#البرمجة غير المتزامنة #Asyncio #Programming Languages #Concurrency #Software Development #Asynchronous Programming

مصادر وروابط خارجية

COMMENTS

التعليقات (0)

كن أول من يعلّق على هذا المقال.

أضف تعليقك

يظهر تعليقك بعد المراجعة.