Python Programming

كسر حاجز السرعة في بايثون: غوص عميق في المعالجة المتزامنة والمتوازية

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

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

التزامن مقابل التوازي: تعريف المصطلحات

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

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

الخيوط (Threading): بطل المهام المحدودة بـ I/O

يُعد وحدة threading في بايثون النهج التقليدي للتزامن. ومع ذلك، بسبب وجود GIL، لا تناسب الخيوط المهام المحدودة بـ CPU. إذا قمت بإنشاء خمس خيوط لأداء حسابات رياضية مكثفة، فمن المرجح أن يتم تنفيذها تسلسلياً، واحدة تلو الأخرى، لأن خيطاً واحداً فقط يمكنه الاحتفاظ بـ GIL في أي لحظة معينة.

ومع ذلك، عندما يتعلق الأمر بـ المهام المحدودة بـ I/O (مثل طلبات الشبكة، أو قراءة الملفات، أو استعلامات قواعد البيانات)، تكون الخيوط فعالة للغاية. عندما ينتظر خيط حدوث I/O، فإنه يحرر GIL، مما يسمح للخيوط الأخرى بالعمل. يجعل هذا الخيوط مثالية لبناء خوادم شبكية عالية الإنتاجية أو برامج جمع البيانات (Scrapers).

إليك مثال بسيط باستخدام concurrent.futures لتحميل صفحات ويب متعددة بشكل متزامن:

import concurrent.futures
import urllib.request

URLS = ['http://www.foxnews.com/', 'http://www.cnn.com/', 'http://espn.com/']

def load_url(url, timeout):
    with urllib.request.urlopen(url, timeout=timeout) as conn:
        return conn.read()

with concurrent.futures.ThreadPoolExecutor(max_workers=5) as executor:
    future_to_url = {executor.submit(load_url, url, 60): url for url in URLS}
    for future in concurrent.futures.as_completed(future_to_url):
        url = future_to_url[future]
        try:
            data = future.result()
        except Exception as exc:
            print('%r generated an exception: %s' % (url, exc))
        else:
            print('%r page is %d bytes' % (url, len(data)))

التعدد (Multiprocessing): تجاوز GIL للمهام المعالجة بـ CPU

بالنسبة للمهام المحدودة بـ CPU، مثل معالجة الصور، أو تحليل البيانات، أو المحاكاة المعقدة، لن توفر الخيوط مكاسب في الأداء. لتحقيق التوازي الحقيقي، يجب عليك استخدام عمليات متعددة. توفر وحدة multiprocessing نسخة محلية من مفسر بايثون لكل عملية، متجاوزةً بذلك GIL تماماً.

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

from multiprocessing import Pool

def square(n):
    return n * n

if __name__ == '__main__':
    numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
    with Pool(4) as p:
        results = p.map(square, numbers)
    print(results)

Asyncio: المعيار الحديث للتزامن مع I/O

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

يتيح لك Asyncio كتابة كود غير متزامن يبدو متزامناً، مما يحسن قابلية القراءة مع الحفاظ على الأداء العالي.

import asyncio
import aiohttp

async def fetch_url(session, url):
    async with session.get(url) as response:
        return await response.text()

async def main():
    async with aiohttp.ClientSession() as session:
        tasks = [fetch_url(session, url) for url in ['http://example.com', 'http://python.org']]
        results = await asyncio.gather(*tasks)
        print(results)

asyncio.run(main())

الخاتمة

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

من خلال فهم هذه الأدوات ومفاضلاتها، يمكنك كتابة تطبيقات بايثون التي تكون ليست فقط صحيحة، بل وأيضاً عالية الأداء وقابلة للتوسع. توقف عن السماح لـ GIL بتقييد إمكاناتك؛ ابدأ بهندسة الأنظمة للتوازي اليوم.

Share: