Python غير المتزامن مقابل الخيوط: متى تستخدم asyncio
لماذا يهم التزامن في بايثون
لديك سكربت بايثون يجلب البيانات من واجهات برمجة تطبيقات متعددة. إنه يعمل، لكنه بطيء—كل طلب ينتظر انتهاء الطلب السابق. سمعت عن asyncio والخيوط، لكن أيهما يجب أن تستخدم؟ اختيار النموذج الخاطئ يمكن أن يؤدي إلى كود إما معقد بشكل غير ضروري أو بطيء بشكل مؤلم.
هذا الدليل يزيل الغموض. سنقارن بين نماذج التزامن غير المتزامن والخيوط في بايثون، نوضح لك بالضبط متى تستخدم كل منهما، ونقدم أمثلة عملية يمكنك تكييفها لمشاريعك الخاصة.
فهم مشهد التزامن في بايثون
تقدم بايثون ثلاثة نماذج رئيسية للتزامن:
- الخيوط (Threads) – خيوط تنفيذ متعددة داخل عملية واحدة، يديرها نظام التشغيل. محدودة بقفل المفسر العالمي (GIL) للعمل المرتبط بالمعالجة.
- غير المتزامن (asyncio) – حلقة أحداث أحادية الخيط تتنقل بين المهام عند نقاط
await. ممتازة للعمليات المرتبطة بالإدخال/الإخراج. - تعدد العمليات (Multiprocessing) – عمليات منفصلة، كل منها بمفسر بايثون ومساحة ذاكرة خاصة. يتجاوز GIL لتحقيق التوازي الحقيقي.
GIL هو قفل متبادل يمنع خيوط متعددة من تنفيذ بايت كود بايثون في وقت واحد. هذا يعني أن الخيوط لا تسرع المهام المرتبطة بالمعالجة، لكنها تساعد في المهام المرتبطة بالإدخال/الإخراج لأن GIL يتم تحريره أثناء عمليات الإدخال/الإخراج.
متى تستخدم asyncio
asyncio يتألق عندما يقضي برنامجك معظم وقته في انتظار أحداث خارجية: طلبات الشبكة، إدخال/إخراج الملفات، استعلامات قواعد البيانات، أو المؤقتات. لأنه يستخدم خيطًا واحدًا، تتجنب عبء إنشاء الخيوط وتبديل السياق. حلقة الأحداث تدير آلاف الاتصالات المتزامنة بكفاءة.
استخدم asyncio عندما:
- تبني خادم ويب أو عميل يتعامل مع العديد من الاتصالات المتزامنة (مثل FastAPI، aiohttp).
- مهامك مرتبطة بالإدخال/الإخراج وتحتاج إلى تزامن عالٍ (مئات أو آلاف المهام).
- تريد تحكمًا دقيقًا في جدولة المهام وإلغائها.
- تستخدم بالفعل إطار عمل أو مكتبة غير متزامنة.
إليك مثال بسيط يجلب عناوين URL متعددة بشكل متزامن:
import asyncio
import aiohttp
async def fetch(url):
async with aiohttp.ClientSession() as session:
async with session.get(url) as response:
return await response.text()
async def main():
urls = [
'https://example.com',
'https://example.org',
'https://example.net',
]
tasks = [fetch(url) for url in urls]
results = await asyncio.gather(*tasks)
print(f'Fetched {len(results)} pages')
asyncio.run(main())
متى تستخدم الخيوط
الخيوط مناسبة عندما يكون لديك عمليات إدخال/إخراج حاجبة ليس لها مكافئات غير متزامنة، أو عندما تعمل مع كود قديم غير متوافق مع النمط غير المتزامن. كما أنها أبسط في الفهم لعدد صغير من المهام.
استخدم الخيوط عندما:
- تحتاج إلى تشغيل إدخال/إخراج حاجب بالتوازي، مثل قراءة ملفات متعددة أو إجراء استدعاءات API متزامنة.
- تتكامل مع مكتبة لا تدعم النمط غير المتزامن.
- لديك عدد معتدل من المهام المتزامنة (عشرات، وليس آلاف).
- تريد الحفاظ على كودك متزامنًا ولكن مع تداخل الإدخال/الإخراج.
مثال باستخدام concurrent.futures.ThreadPoolExecutor:
from concurrent.futures import ThreadPoolExecutor
import requests
def fetch(url):
response = requests.get(url)
return response.text
urls = [
'https://example.com',
'https://example.org',
'https://example.net',
]
with ThreadPoolExecutor(max_workers=10) as executor:
results = list(executor.map(fetch, urls))
print(f'Fetched {len(results)} pages')
مقارنة asyncio والخيوط
إليك مقارنة جنبًا إلى جنب لمساعدتك في اتخاذ القرار:
| الجانب | asyncio | الخيوط |
|---|---|---|
| نموذج التزامن | حلقة أحداث أحادية الخيط | خيوط نظام تشغيل متعددة |
| الأفضل لـ | مرتبط بالإدخال/الإخراج، تزامن عالٍ | مرتبط بالإدخال/الإخراج، استدعاءات حاجبة |
| أداء المهام المرتبطة بالمعالجة | ضعيف (GIL) | ضعيف (GIL) |
| قابلية التوسع | آلاف المهام | مئات الخيوط |
| التعقيد | يتطلب صيغة async/await | نمط متزامن مألوف |
| تصحيح الأخطاء | قد يكون صعبًا مع التتبع | أدوات تصحيح قياسية |
| النظام البيئي | دعم غير متزامن متزايد | عالمي |
ماذا عن المهام المرتبطة بالمعالجة؟
لا asyncio ولا الخيوط ستساعدك في موازاة العمل المكثف للمعالجة في بايثون بسبب GIL. للمهام المرتبطة بالمعالجة—مثل حسابات الأرقام، معالجة الصور، أو ضغط البيانات—استخدم multiprocessing أو concurrent.futures.ProcessPoolExecutor.
مثال:
from concurrent.futures import ProcessPoolExecutor
def cpu_heavy(n):
return sum(i * i for i in range(n))
if __name__ == '__main__':
with ProcessPoolExecutor() as executor:
results = list(executor.map(cpu_heavy, [10**6, 10**6, 10**6]))
print(results)
أفضل الممارسات والمزالق
- لا تخلط الاستدعاءات الحاجبة مع asyncio. إذا استدعيت دالة حاجبة داخل مهمة غير متزامنة، فستحجب حلقة الأحداث بأكملها. استخدم
asyncio.to_thread()لتشغيل الكود الحاجب في مجموعة خيوط. - حدد حجم مجموعة الخيوط. إنشاء عدد كبير جدًا من الخيوط يؤدي إلى عبء تبديل السياق. استخدم
ThreadPoolExecutor(max_workers=N). - تعامل مع الاستثناءات بشكل صحيح. في asyncio، قد تمر الاستثناءات غير المعالجة في المهام دون أن يلاحظها أحد. استخدم
asyncio.gather(..., return_exceptions=True)أو أضف ردود نداء. - فضل asyncio للمشاريع الجديدة كثيفة الإدخال/الإخراج. النظام البيئي ناضج، وأطر العمل مثل FastAPI و aiohttp تسهل الأمر.
- استخدم الخيوط للسكربتات القديمة أو البسيطة. إذا كنت تحتاج فقط إلى موازاة بعض الاستدعاءات الحاجبة، فإن الخيوط أقل تدخلاً.
الأسئلة الشائعة
هل يمكنني استخدام asyncio والخيوط معًا؟
نعم. يمكنك تشغيل الكود الحاجب في مجموعة خيوط باستخدام asyncio.to_thread() أو loop.run_in_executor(). هذا مفيد عندما تحتاج إلى استدعاء مكتبة متزامنة من كود غير متزامن دون حجب حلقة الأحداث.
هل يعمل asyncio مع GIL؟
نعم، asyncio يعمل في خيط واحد ويخضع لـ GIL. ومع ذلك، لأنه مصمم للمهام المرتبطة بالإدخال/الإخراج، يتم تحرير GIL أثناء عمليات الإدخال/الإخراج، مما يسمح للمهام الأخرى بالعمل. للعمل المرتبط بالمعالجة، لا يقدم asyncio أي تسريع.
أيهما أسرع: asyncio أم الخيوط؟
للمهام المرتبطة بالإدخال/الإخراج مع العديد من العمليات المتزامنة، عادةً ما يكون asyncio أسرع وأكثر قابلية للتوسع لأنه يتجنب عبء الخيوط. لعدد صغير من المهام الحاجبة، قد تكون الخيوط أبسط وتؤدي بشكل مشابه. لا يساعد أي منهما في المهام المرتبطة بالمعالجة.
اتخاذ القرار الصحيح
ابدأ بتحديد ما إذا كانت عنق الزجاجة لديك هي الإدخال/الإخراج أو المعالجة. إذا كانت الإدخال/الإخراج وتحتاج إلى تزامن عالٍ، فاستخدم asyncio. إذا كانت الإدخال/الإخراج ولكنك تتعامل مع مكتبات حاجبة أو سكربتات أبسط، فإن الخيوط خيار قوي. للعمل المرتبط بالمعالجة، استخدم multiprocessing.
تذكر أنه يمكنك غالبًا دمج هذه النماذج—على سبيل المثال، استخدام asyncio لعمليات الشبكة ومجموعة خيوط لإدخال/إخراج الملفات. المفتاح هو فهم المقايضات واختيار الأداة التي تناسب مشكلتك المحددة.
عندما تحتاج إلى تنسيق أو التحقق من صحة بيانات JSON التي تم إرجاعها من استدعاءات API المتزامنة، جرب JSON Formatter لدينا لطباعة وتصحيح الأخطاء بسهولة.