5.2البرمجة غير المتزامنة (asynchronous programming) (asyncio)
الأساسي في 30 ثانية
- غير المتزامن (Async) = استغلال الانتظار : بينما ينتظر (API، قاعدة، ملفات)، يفعل البرنامج شيئا آخر بدل البقاء محجوبا.
- الثلاثي : async def يصرح، وawait ينتظر دون حجب، وgather يطلق معا — وasyncio.run في القمة.
- عوالم منفصلة : المحجوب (requests، sleep) يشل غير المتزامن — مكتبات متوافقة مع async أو لا شيء.
حالة مصغرة متسلسلة — نبيل يسرع جمعه في الجزائر
نبيل، محلل بيانات مبتدئ في الجزائر، يجمع كل صباح بيانات من عدة مصادر وب : بالتسلسل، ينتظر سكريبته كل جواب واحدا بعد الآخر ويدوم الجمع أبدا. أعاد كتابة الحلقة كروتينات (coroutines) مطلقة معا مع asyncio.gather : تتداخل الانتظارات بدل أن تتجمع. تتمة الحالة 5.1 — يشغل نبيل الأوقات الميتة بدل تحملها.
تتمة الحالة 5.1 — يمر نبيل من التسلسلي الذي ينتظر إلى المتزامن الذي يغطي.
1. حين يهيمن الانتظار : غير المتزامن
API، وقاعدة بيانات، وملفات بعيدة : ينتظر البرنامج أكثر مما يحسب. تحرر البرمجة غير المتزامنة الخيط أثناء الانتظار : بدل البقاء محجوبا، يمر إلى مهام أخرى، وخيط واحد يدير آلاف الطلبات المتزامنة دون آلاف الخيوط. يجد نبيل فيها جمعه الصباحي : العنق هو انتظار الشبكة، لا الحساب.
حد واضح : لحساب خالص (مقيد بالمعالج CPU-bound)، لا يسرع غير المتزامن شيئا — إنه multiprocessing. غير المتزامن لـ I/O، و multiprocessing لـ CPU : الاختيار حسب العنق، لا حسب الموضة.
2. الثلاثي : async def و await و gather
async def fetch(url) يصرح بروتين (coroutine)، وawait ينتظر دون حجب (يُحرَّر الخيط لمهام أخرى)، وasyncio.gather(*taches) يطلق معا وينتظر الجميع. وasyncio.run(main()) ينظم من القمة عبر حلقة الأحداث. أطلق نبيل جمعه في gather : ثلاثة انتظارات تتداخل بدل أن تتجمع.
الفخ الكلاسيكي : نسيان await يجعل روتينا (coroutine) لا يُنفَّذ أبدا (تحذير خفي، ونتيجة فارغة). كل روتين مُنشأ يجب انتظاره أو تجميعه.
3. عدم خلط العوالم
المحجوب في غير المتزامن يشل كل شيء : يجمد time.sleep الخيط، ويلغي requests (المتزامن) الفائدة — استعملوا asyncio.sleep ومكتبات متوافقة مع async (httpx، aiohttp). بقية محجوبة ناجية تُعزَل عبر run_in_executor. استبدل نبيل requests بعميل async : دون ذلك، كان gather ينتظر مع ذلك.
القاعدة الذهبية : غير متزامن من الطرف إلى الطرف أو لا شيء — المختلط هو أسوأ العوالم. ونقيس قبل/بعد لإثبات المكسب بدل افتراضه.
هل تعلم ؟
روتين مُنشأ، ولم يُنتظَر أبدا : عمل موعود، ولم يُنجَز أبدا.
🔎 مثال ملموس — يجمع نبيل مصادره بالتوازي
تتداخل الانتظارات بدل أن تتجمع.
import asyncio
async def collecter(source):
print(f"collecte {source}...")
await asyncio.sleep(1)
return f"{source} : ok"
async def main(sources):
return await asyncio.gather(*(collecter(s) for s in sources))
res = asyncio.run(main(["api", "fichier", "base"]))
print(res)
القراءة : ثلاثة انتظارات ثانية واحدة تستغرق ثانية واحدة تقريبا في المجموع، لا ثلاثا.
الدرس : في إنتاج الشبكة، استبدلوا الـ sleep بعميل async (httpx/aiohttp).
مفردات القسم
- البرمجة غير المتزامنة
- نموذج يشغل البرنامج أثناء أوقات الانتظار بدل حجبه.
مثال بسيط: يجمع نبيل ثلاثة مصادر معا بدل انتظار كل واحد.
ما ليس كذلك: عدة معالجات تحسب بالتوازي.
رد فعل: هل ينتظر برنامجي أكثر مما يحسب ؟
- async / await
- يصرح async بروتين (coroutine) ؛ وينتظر await دون حجب الخيط.
مثال بسيط: ينتظر نبيل (await) كل جمع لتحرير الخيط.
ما ليس كذلك: نداء محجوب يجمد كل شيء.
رد فعل: هل نسيت await (روتين شبح) ؟
- asyncio.gather
- يطلق عدة روتينات (coroutines) معا وينتظر الجميع.
مثال بسيط: جمع نبيل جموعه الثلاثة في gather واحد.
ما ليس كذلك: حلقة تنتظر كل مهمة واحدة بعد الأخرى.
رد فعل: هل يمكن أن تتداخل هذه الانتظارات ؟
- الروتين (Coroutine)
- دالة async قابلة للتعليق والاستئناف من حلقة الأحداث.
مثال بسيط: دالة الجمع عند نبيل، المعلقة أثناء الـ sleep.
ما ليس كذلك: دالة عادية تحجب حتى النهاية.
رد فعل: من ينتظر هذا الروتين ؟
- مقيد بـ I/O مقابل مقيد بالمعالج
- الانتظار (شبكة/قرص) ← asyncio ؛ والحساب المكثف ← multiprocessing.
مثال بسيط: أخذ نبيل asyncio لواجهات API، لا لحساب ضخم.
ما ليس كذلك: async لتسريع حساب خالص.
رد فعل: هل عنقي الانتظار أم الحساب ؟
في التطبيق — اكتشاف async/await مع asyncio
- اكتبوا روتينا (coroutine) « async def tache(n) » يعرض رسالة، وينتظر (await asyncio.sleep(1)) ثم يعرض النهاية.
- أطلقوه مع asyncio.run() ولاحظوا السلوك التسلسلي لمهمة واحدة.
- أطلقوا عدة مهام بالتوازي مع asyncio.gather() : لاحظوا أن 3 انتظارات 1 ث تأخذ ~1 ث في المجموع، لا 3.
- استنتجوا : يلمع غير المتزامن حين ننتظر (شبكة، I/O)، لا لتسريع حساب مكثف.
نص جاهز للقول — تبرير غير المتزامن
« في الجزائر، جمعنا الصباحي ينتظر كل مصدر واحدا بعد الآخر : العنق هو انتظار الشبكة، لا الحساب. أقترح تمرير الحلقة إلى روتينات (coroutines) مع gather وعميل async : ستتداخل الانتظارات. أقيس قبل/بعد لإثبات المكسب، وأبقي الحساب الخالص خارج غير المتزامن. »
يُقال للمسؤول لتمرير إعادة كتابة مستهدفة ومقاسة، دون لمس الحساب.
نموذج نسخ-لصق — جمع async عام
import asyncio
async def tache(nom):
print(f"debut {nom}")
await asyncio.sleep(1)
print(f"fin {nom}")
return nom
async def main():
return await asyncio.gather(*(tache(n) for n in ["a", "b", "c"]))
if __name__ == "__main__":
print(asyncio.run(main()))
# في إنتاج الشبكة : استبدال asyncio.sleep بـ httpx/aiohttp (متوافقة مع async).
مستقل / ميزانية صغيرة
نبيل وحده : يكفي gather على مصادره الموجودة — دون بنية جديدة، فقط روتينات (coroutines).
مقاولة صغيرة / فريق مصغر
عميل HTTP غير متزامن (async) مشترك : يجمع الفريق كله دون مضاعفة الجلسات المحجوبة.
مقاولة متوسطة في الجزائر
يقيس نبيل قبل/بعد في الجزائر على الجمع الحقيقي : المكسب المثبت يبرر إعادة الكتابة.
فريق يبدأ
ثنائي : أحدهما يحول مصدرا إلى async، والآخر يتعقب النداءات المحجوبة الباقية.
✓ ممارسات جيدة
- غير المتزامن لـ I/O، و multiprocessing للمعالج : الاختيار حسب العنق.
- gather للإطلاق معا ؛ ومكتبات متوافقة مع async (httpx).
- القياس قبل/بعد : إثبات المكسب بدل افتراضه.
✗ أخطاء شائعة
- نداء requests (المحجوب) في غير المتزامن.
- نسيان await : روتين شبح لا يُنفَّذ أبدا.
- وضع async على حساب خالص (لا مكسب، وتعقيد مهدى).
النقاط الأساسية
- يشغل غير المتزامن البرنامج أثناء أوقات الانتظار بدل حجبه.
- يعرّف async روتينا (coroutine) ؛ ويعلق await منتظرا نتيجة.
- يطلق asyncio.gather() عدة مهام انتظار بالتوازي.
- يخدم asyncio حالات I/O-bound (شبكة، قرص)، لا الحسابات المكثفة (← multiprocessing).
تحدي 48 ساعة — طبق غدا
حولوا حلقة انتظار حقيقية واحدة (API، ملفات) إلى gather : هل تأخذ الانتظارات الثلاثة 3 أضعاف الوقت بعد ؟
- أستطيع شرح async/await/gather في جملة واحدة.
- عرض gather عندي يغطي الانتظارات (مثبت بالقياس).
- أعرف لماذا requests محظور في غير المتزامن.
أسئلة شائعة
غير المتزامن والخيوط والعمليات المتعددة : ما الفرق ؟
ثلاثة أجوبة لثلاثة اختناقات. يدير asyncio تزامن انتظارات I/O في خيط واحد (مثالي شبكة/قرص). ويتقاسم الخيوط (threading) أيضا عملية واحدة لكن يناسب بعض I/O المحجوبة. ويطلق تعدد العمليات (multiprocessing) عدة عمليات لموازاة الحساب على عدة أنوية. نختار حسب ما إذا كان العنق الانتظار (asyncio) أو الحساب (multiprocessing).
هل يجعل غير المتزامن البرنامج أسرع حتما ؟
لا. يسرع الأحمال التي يهيمن عليها الانتظار (كثير طلبات شبكة متزامنة). لحساب خالص (مقيد بالمعالج CPU-bound)، لا يجلب شيئا، بل قد يعقد الشيفرة دون داع. استعملوه في محله، حيث توجد أوقات ميتة لاستغلالها — وقيسوا لإثباته.
في أي حالة يجلب asyncio مكسبا ؟