فلسفة الكود
التزامن الأصلي أولاً
١٣ أكتوبر ٢٠٢٦ · 6 دقائق قراءة
البداية: gather هي الرد الأول الخاطئ
لما بتواجهك مجموعة مهام مستقلة، أول ما يخطر في بالك هو asyncio.gather:
async def main():
await asyncio.gather(
fetch_a(), fetch_b(), fetch_c(),
)
وهي مش غلط في حد ذاتها — بس في حالات كتير هي الخيار الخشبي: بتعمل تزامن بنفسك، في حين أن المكتبة اللي هتتكلم معها واعية بالتزامن أكتر منك. غيّر ترتيب تفكيرك:
- هل فيه أمر واحد للمكتبة بياخد كله؟ → استخدمه.
- هل المكتبة بتدعم batch / pipeline؟ → استخدمه.
- هل فيه عميل متزامن (
AsyncClient,aiohttp)؟ → استخدم الأصل مباشرة. - وفشل كل الخيارات السابقة → gather والحل اليدوي.
Redis Pipeline: التزامن اللي كان جوه المكتبة
أوضح مثال. عملت ميزة كانت محتاجة تعدّ/تقرأ مجموعة مفاتيح، ورد الفعل الأول كان:
results = await asyncio.gather(
*(client.get(key) for key in keys)
)
صح تقنيًا، بس غلط هندسيًا: كل أمر طلب، وكل واحد رحلة ذهاب وإياب منفصلة لها ثمن. الحل الأصلي اللي عرفت إنه موجود: pipeline بيعمل نفس الدفعة في رحلة شبكة واحدة:
pipe = client.pipeline()
for key in keys:
pipe.get(key)
results = await pipe.execute() # طلب واحد، نتايج عشرات
نفس النتيجة، مهندسة صح، وفارق ملحوظ في الربط الفعلي.
httpx limits و aiohttp Connector
مثال تاني من جمع بيانات: عايز أرمي 100 طلب على مصدر واحد. gather بيبعتهم كلهم في نفس اللحظة → الخادم ينهار. الحل مش إدارة semaphore بإيدك فقط، الحل كمان إن العميل نفسه بيقدم حدود أدق (concurrency bound لكل host):
limits = httpx.Limits(max_connections=10, max_keepalive_connections=10)
async with httpx.AsyncClient(limits=limits) as client:
...
لما gather صح فعلًا
قاعدة «اسأل المكتبة الأول» بتنطبق هنا كمان: gather تبقى صح لما الشغل مستقل بلا مكتبة بتدير تزامنها — مثلاً مهام محلية (شغل على صور محفوظة) أو طلبات على خدمات متفرقة، لكل واحدة عميل خاص. في الحالات دي gather مش وسيلة مؤقتة — هي الحل الوحيد.
الخلاصة
ابدأ بسؤال «المكتبة بتفهّم التزامن ولا لأ» مش بسؤال «أرمي gather ولا loop». التزامن الأصلي مش لأنه أسرع بس — لأنه بيضع الحدود والقرارات في المكان الصح، ويحتفظ بكود أقل نحتاج نتحكم فيه بإيدنا.