جدول المحتويات

  • ما المشكلة التي حلها GIL لبايثون؟
  • لماذا تم اختيار GIL كحل؟
  • التأثير على برامج بايثون متعددة الخيوط
  • لماذا لم تتم إزالة GIL حتى الآن؟
  • لماذا لم تتم إزالته في Python 3؟
  • كيفية التعامل مع Python’s GIL

إن قفل Python Global Interpreter Lock أو GIL ، بكلمات بسيطة ، هو كائن المزامنة (أو القفل) الذي يسمح لخيط واحد فقط بالسيطرة على مترجم Python.

هذا يعني أن مؤشر ترابط واحد فقط يمكن أن يكون في حالة تنفيذ في أي وقت. لا يكون تأثير GIL مرئيًا للمطورين الذين ينفذون برامج أحادية السلسلة ، ولكن يمكن أن يكون عقبة في الأداء في التعليمات البرمجية المرتبطة بوحدة المعالجة المركزية والمتعددة المسارات.

نظرًا لأن GIL يسمح فقط بتنفيذ مؤشر ترابط واحد في كل مرة حتى في بنية متعددة الخيوط مع أكثر من نواة وحدة المعالجة المركزية ، فقد اكتسب GIL سمعة باعتباره ميزة “سيئة السمعة” في Python.

ستتعرف في هذه المقالة على كيفية تأثير GIL على أداء برامج Python الخاصة بك ، وكيف يمكنك التخفيف من التأثير الذي قد يحدثه على التعليمات البرمجية الخاصة بك.

ما المشكلة التي حلها GIL لبايثون؟

يستخدم Python حساب المرجع لإدارة الذاكرة. هذا يعني أن الكائنات التي تم إنشاؤها في Python لها متغير عدد مرجعي يتتبع عدد المراجع التي تشير إلى الكائن. عندما يصل هذا العدد إلى الصفر ، يتم تحرير الذاكرة التي يشغلها الكائن.

دعنا نلقي نظرة على مثال رمز مختصر لشرح كيفية عمل العد المرجعي:

>>> import sys
>>> a = []
>>> b = a
>>> sys.getrefcount(a)
3

في المثال أعلاه ، كان عدد المراجع لكائن القائمة الفارغة [] هو 3. تمت الإشارة إلى كائن القائمة بواسطة a و b وتم تمرير الوسيطة إلى sys.getrefcount ().

العودة إلى GIL:

كانت المشكلة أن متغير العد المرجعي هذا يحتاج إلى الحماية من ظروف السباق حيث يزيد خيطان قيمته أو ينقصان في وقت واحد. إذا حدث هذا ، فقد يتسبب في حدوث تسرب للذاكرة التي لم يتم إصدارها مطلقًا ، أو الأسوأ من ذلك ، تحرير الذاكرة بشكل غير صحيح بينما لا تزال الإشارة إلى هذا الكائن موجودة. يمكن أن يتسبب هذا في حدوث أعطال أو أخطاء أخرى “غريبة” في برامج بايثون.

يمكن الحفاظ على متغير عدد المرجع هذا آمنًا عن طريق إضافة أقفال إلى جميع هياكل البيانات المشتركة عبر سلاسل الرسائل بحيث لا يتم تعديلها بشكل غير متسق.

لكن إضافة قفل لكل كائن أو مجموعة من الكائنات يعني وجود أقفال متعددة والتي يمكن أن تسبب مشكلة أخرى – Deadlocks (يمكن أن تحدث حالات التوقف التام فقط إذا كان هناك أكثر من قفل واحد). يتمثل أحد الآثار الجانبية الأخرى في انخفاض الأداء الناجم عن الاستحواذ المتكرر وإطلاق الأقفال.

GIL هو قفل واحد على المترجم الفوري نفسه والذي يضيف قاعدة مفادها أن تنفيذ أي كود بايت Python يتطلب الحصول على قفل المترجم الفوري. هذا يمنع حالات الجمود (حيث لا يوجد سوى قفل واحد) ولا يقدم الكثير من الأداء الزائد. لكنه يجعل أي برنامج Python مرتبط بوحدة المعالجة المركزية مترابطًا بشكل فعال.

على الرغم من استخدام GIL من قبل المترجمين الفوريين للغات أخرى مثل Ruby ، ​​إلا أنه ليس الحل الوحيد لهذه المشكلة. تتجنب بعض اللغات مطلب GIL لإدارة الذاكرة الآمنة باستخدام طرق أخرى غير حساب المرجع ، مثل جمع البيانات المهملة.

من ناحية أخرى ، هذا يعني أنه يتعين على هذه اللغات في كثير من الأحيان تعويض فقدان مزايا الأداء المترابط الفردي لـ GIL عن طريق إضافة ميزات أخرى لتعزيز الأداء مثل مترجمي JIT.

لماذا تم اختيار GIL كحل؟

إذن ، لماذا تم استخدام أسلوب يبدو أنه يعيق استخدام لغة بايثون؟ هل كان قرارًا سيئًا من قِبل مطوري Python؟

حسنًا ، على حد تعبير لاري هاستينغز ، فإن قرار تصميم GIL هو أحد الأشياء التي جعلت بايثون مشهورة كما هي اليوم.

كانت Python موجودة منذ الأيام التي لم يكن لدى أنظمة التشغيل فيها مفهوم الخيوط. تم تصميم Python ليكون سهل الاستخدام من أجل جعل التطوير أسرع وبدأ المزيد والمزيد من المطورين في استخدامه.

تمت كتابة الكثير من الإضافات لمكتبات C الحالية التي كانت هناك حاجة إلى ميزاتها في Python. لمنع التغييرات غير المتسقة ، تتطلب امتدادات C هذه إدارة ذاكرة آمنة للخيط والتي قدمها GIL.

GIL سهل التنفيذ ويمكن إضافته بسهولة إلى Python. إنه يوفر زيادة في أداء البرامج أحادية الخيوط حيث يلزم إدارة قفل واحد فقط.

أصبح من السهل دمج مكتبات C التي لم تكن آمنة بمؤشر الترابط. وأصبحت امتدادات C هذه أحد الأسباب التي جعلت المجتمعات المختلفة تتبنى لغة Python بسهولة.

كما ترى ، كان GIL حلاً عمليًا لمشكلة صعبة واجهها مطورو CPython في وقت مبكر من حياة Python.

التأثير على برامج بايثون متعددة الخيوط

عندما تنظر إلى برنامج Python نموذجي – أو أي برنامج كمبيوتر لهذا الأمر – هناك فرق بين تلك المرتبطة بوحدة المعالجة المركزية في أدائها وتلك المرتبطة بـ I / O.

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

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

دعنا نلقي نظرة على برنامج بسيط مرتبط بوحدة المعالجة المركزية يقوم بإجراء عد تنازلي:

# single_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n>0:
        n -= 1

start = time.time()
countdown(COUNT)
end = time.time()

print('Time taken in seconds -', end - start)

أعطى تشغيل هذا الكود على نظامي باستخدام 4 مراكز الإخراج التالي:

$ python single_threaded.py
Time taken in seconds - 6.20024037361145

الآن قمت بتعديل الكود قليلاً لأفعله لنفس العد التنازلي باستخدام خيطين متوازيين:

# multi_threaded.py
import time
from threading import Thread

COUNT = 50000000

def countdown(n):
    while n>0:
        n -= 1

t1 = Thread(target=countdown, args=(COUNT//2,))
t2 = Thread(target=countdown, args=(COUNT//2,))

start = time.time()
t1.start()
t2.start()
t1.join()
t2.join()
end = time.time()

print('Time taken in seconds -', end - start)

وعندما قمت بتشغيله مرة أخرى:

$ python multi_threaded.py
Time taken in seconds - 6.924342632293701

كما ترى ، يستغرق كلا الإصدارين نفس القدر من الوقت تقريبًا للانتهاء. في الإصدار متعدد الخيوط ، منع GIL الخيوط المرتبطة بوحدة المعالجة المركزية من التنفيذ في parellel.

لا يؤثر GIL كثيرًا على أداء البرامج متعددة مؤشرات الترابط المرتبطة بالإدخال / الإخراج حيث تتم مشاركة القفل بين مؤشرات الترابط أثناء انتظار الإدخال / الإخراج.

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

هذه الزيادة هي نتيجة اكتساب وإطلاق النفقات العامة المضافة بواسطة القفل.

لماذا لم تتم إزالة GIL حتى الآن؟

يتلقى مطورو Python الكثير من الشكاوى بخصوص هذا ، لكن لغة شائعة مثل Python لا يمكنها إحداث تغيير مهم مثل إزالة GIL دون التسبب في مشاكل عدم التوافق مع الإصدارات السابقة.

من الواضح أنه يمكن إزالة GIL وقد تم القيام بذلك عدة مرات في الماضي من قبل المطورين والباحثين ولكن كل هذه المحاولات كسرت امتدادات C الحالية التي تعتمد بشكل كبير على الحل الذي توفره GIL.

بالطبع ، هناك حلول أخرى للمشكلة التي يحلها GIL ولكن بعضها يقلل من أداء برامج الإدخال / الإخراج ذات الخيوط المفردة والمتعددة الخيوط وبعضها صعب للغاية. بعد كل شيء ، لا تريد أن تعمل برامج Python الحالية بشكل أبطأ بعد ظهور إصدار جديد ، أليس كذلك؟

أعطى مبتكر Python و BDFL ، Guido van Rossum ، إجابة للمجتمع في سبتمبر 2007 في مقالته “ليس من السهل إزالة GIL”:

“أرحب بمجموعة من التصحيحات في Py3k فقط إذا لم ينخفض ​​أداء برنامج أحادي السلسلة (وبرنامج متعدد الخيوط ولكن مرتبط بإدخال / إخراج)”

وهذا الشرط لم يتم الوفاء به بأي من المحاولات التي تمت منذ ذلك الحين.

لماذا لم تتم إزالته في Python 3؟

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

ولكن لماذا لم يتم إزالة GIL جنبًا إلى جنب؟

كان من الممكن أن تؤدي إزالة GIL إلى جعل Python 3 أبطأ مقارنةً بـ Python 2 في الأداء أحادي السلسلة ويمكنك تخيل ما كان سينتج عنه. لا يمكنك المجادلة مع مزايا الأداء أحادية السلسلة لـ GIL. وبالتالي فإن النتيجة هي أن Python 3 لا يزال لديه GIL.

لكن Python 3 جلبت تحسينًا كبيرًا على GIL الحالي –

ناقشنا تأثير GIL على البرامج متعددة الخيوط “المرتبطة فقط بوحدة المعالجة المركزية” و “I / O فقط” ولكن ماذا عن البرامج التي تكون فيها بعض سلاسل العمليات مرتبطة بـ I / O وبعضها مرتبط بوحدة المعالجة المركزية؟

في مثل هذه البرامج ، عُرف عن Python’s GIL بتجويع سلاسل الإدخال / الإخراج من خلال عدم منحها فرصة للحصول على GIL من سلاسل العمليات المرتبطة بوحدة المعالجة المركزية.

كان هذا بسبب آلية مدمجة في Python أجبرت الخيوط على تحرير GIL بعد فترة زمنية ثابتة من الاستخدام المتواصل وإذا لم يحصل أي شخص آخر على GIL ، فيمكن لنفس الخيط مواصلة استخدامه.

>>> import sys
>>> # The interval is set to 100 instructions:
>>> sys.getcheckinterval()
100

كانت المشكلة في هذه الآلية هي أن الخيط المرتبط بوحدة المعالجة المركزية في معظم الأحيان سوف يستعيد GIL نفسه قبل أن تتمكن الخيوط الأخرى من الحصول عليه. تم البحث عن هذا بواسطة David Beazley ويمكن العثور على التصورات هنا.

تم إصلاح هذه المشكلة في Python 3.2 في عام 2009 من قبل Antoine Pitrou الذي أضاف آلية للنظر في عدد طلبات الاستحواذ على GIL من خلال مؤشرات الترابط الأخرى التي تم إسقاطها وعدم السماح للخيط الحالي بإعادة الحصول على GIL قبل أن تحصل الخيوط الأخرى على فرصة للتشغيل.

كيفية التعامل مع Python’s GIL

إذا كان GIL يسبب لك مشكلات ، فإليك بعض الطرق التي يمكنك تجربتها:

المعالجة المتعددة مقابل خيوط المعالجة المتعددة: الطريقة الأكثر شيوعًا هي استخدام نهج المعالجة المتعددة حيث تستخدم عمليات متعددة بدلاً من الخيوط. تحصل كل عملية من عمليات Python على مترجم Python الخاص بها ومساحة للذاكرة حتى لا يمثل GIL مشكلة. تحتوي Python على وحدة معالجة متعددة تتيح لنا إنشاء عمليات بسهولة مثل هذا:

from multiprocessing import Pool
import time

COUNT = 50000000
def countdown(n):
    while n>0:
        n -= 1

if __name__ == '__main__':
    pool = Pool(processes=2)
    start = time.time()
    r1 = pool.apply_async(countdown, [COUNT//2])
    r2 = pool.apply_async(countdown, [COUNT//2])
    pool.close()
    pool.join()
    end = time.time()
    print('Time taken in seconds -', end - start)

أعطى تشغيل هذا على نظامي هذا الناتج:

$ python multiprocess.py
Time taken in seconds - 4.060242414474487

زيادة جيدة في الأداء مقارنة بالإصدار متعدد الخيوط ، أليس كذلك؟

لم ينخفض ​​الوقت إلى نصف ما رأيناه أعلاه لأن إدارة العملية لها نفقاتها العامة الخاصة. تعد العمليات المتعددة أثقل من الخيوط المتعددة ، لذا ضع في اعتبارك أن هذا قد يصبح عنق زجاجة.

مفسرات بايثون البديلة: لدى بايثون تطبيقات متعددة للمترجمين الفوريين. CPython و Jython و IronPython و PyPy ، المكتوبة بلغة C و Java و C # و Python على التوالي ، هي الأكثر شيوعًا. يوجد GIL فقط في تطبيق Python الأصلي وهو CPython. إذا كان برنامجك ومكتباته متاحًا لأحد التطبيقات الأخرى ، فيمكنك تجربتها أيضًا.

ما عليك سوى الانتظار: بينما يستفيد العديد من مستخدمي Python من مزايا الأداء الفردي لـ GIL. لا يضطر المبرمجون متعددو الخيوط إلى القلق لأن بعض ألمع العقول في مجتمع Python يعملون على إزالة GIL من CPython. تُعرف إحدى هذه المحاولات باسم استئصال جيلكتوم.

غالبًا ما يُنظر إلى Python GIL على أنه موضوع غامض وصعب. لكن ضع في اعتبارك أنه بصفتك Pythonista ، فأنت عادةً ما تتأثر بها فقط إذا كنت تكتب امتدادات C أو إذا كنت تستخدم خيوط المعالجة المتعددة المرتبطة بوحدة المعالجة المركزية في برامجك.

في هذه الحالة ، يجب أن توفر لك هذه المقالة كل ما تحتاجه لفهم ماهية GIL وكيفية التعامل معها في مشاريعك الخاصة. وإذا كنت تريد فهم الأعمال الداخلية منخفضة المستوى لـ GIL ، فإنني أوصيك بمشاهدة حديث Understanding the Python GIL لـ David Beazley.