Async و Coroutines في Python: ازاي تخدم 10,000 طلب متزامن على core واحد
المستوى: محترف
لو بتجيب بيانات من 50 API endpoint بـ requests.get في loop، الكود بياخد 18 ثانية مجموعها 99% انتظار. سطرين async/await بينزّلوا الزمن لـ 0.7 ثانية على نفس الـ core. الفرق مش في السرعة، الفرق في إن الـ thread وقف يستنى الشبكة ولّا فضل يشتغل.
المشكلة باختصار
أي عملية I/O بتمرّ بثلاث مراحل: تجهيز الطلب، انتظار الشبكة، معالجة الرد. في الكود التزامني الـ thread بيقعد يستنى الشبكة 90% من الوقت بدون أي شغل. لو عندك 1,000 طلب وكل واحد بياخد 200ms في الشبكة، إنت بتدفع 200 ثانية متراكمة، 99% منها وقفة ثابتة.
الـ threads بتحلّ جزء من المشكلة، لكن الـ GIL في CPython بيخلّي thread واحد فقط بيشتغل في أي لحظة على bytecode، والـ context switch مكلف، وكل thread بياخد 8MB stack افتراضيًا. asyncio بيستبدل الـ threads بـ coroutines: دوال خفيفة جدًا (حوالي 3KB لكل واحدة) بتشتغل كلها على event loop واحد بدون قفل أو context switch من الـ kernel.
مثال من الواقع يقرّب الفكرة
تخيّل جارسون في كافيه عنده 10 طاولات. كل طاولة بتاخد 5 دقايق علشان تختار من المنيو. الجارسون التزامني بيقف بجانب الطاولة الأولى مستنّيها تختار، يكتب الطلب، يروح المطبخ، يرجع، ينتقل للطاولة اللي بعدها. النتيجة: 50 دقيقة لخدمة العشر طاولات.
الجارسون اللاتزامني بيدّي القائمة لكل الطاولات الأول، يروح المطبخ، ويرجع كل ما طاولة تنادي. النتيجة: 7 دقايق. هو نفس الشخص بنفس السرعة، لكنه ما بيقفش يستنى. ده بالظبط الفرق بين requests و httpx.AsyncClient.
التعريف العلمي بدقة
الـ Coroutine في Python هي دالة معرّفة بـ async def بترجع Coroutine Object، مش نتيجة فورية. الـ Event Loop بيدير قائمة من الـ coroutines، وكل ما واحدة توصل لـ await بيوقّفها مؤقتًا ويشغّل غيرها. الـ await لازم يبقى فوق primitive يدعم asyncio زي asyncio.sleep، aiohttp request، asyncpg query.
الافتراض الأساسي: الكود اللي جوّا coroutine ما بيعملش CPU work ثقيل. أي عملية بتاخد أكتر من 50ms بدون await هتجمّد كل الـ coroutines التانيين، لأن الـ event loop single-threaded من الأساس.
الكود الحقيقي: قياس فعلي
المقارنة بين requests التزامني و httpx.AsyncClient على 100 طلب لـ httpbin.org بتأخير 200ms لكل واحد: