هل الأقفال غير ضرورية في كود Python متعدد الخيوط بسبب GIL؟

StackOverflow https://stackoverflow.com/questions/105095

  •  01-07-2019
  •  | 
  •  

سؤال

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

إذا كان GIL لا يسمح بتنفيذ تعليمات متعددة بالتوازي، ألن تكون البيانات المشتركة غير ضرورية للحماية؟

آسف إذا كان هذا سؤالًا غبيًا، ولكنه شيء كنت أتساءل عنه دائمًا بشأن Python على الأجهزة متعددة المعالجات/الأجهزة الأساسية.

ينطبق الشيء نفسه على أي تطبيق لغة آخر يحتوي على GIL.

هل كانت مفيدة؟

المحلول

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

على سبيل المثال:

#!/usr/bin/env python
import threading

shared_balance = 0

class Deposit(threading.Thread):
    def run(self):
        for _ in xrange(1000000):
            global shared_balance
            balance = shared_balance
            balance += 100
            shared_balance = balance

class Withdraw(threading.Thread):
    def run(self):
        for _ in xrange(1000000):
            global shared_balance
            balance = shared_balance
            balance -= 100
            shared_balance = balance

threads = [Deposit(), Withdraw()]

for thread in threads:
    thread.start()

for thread in threads:
    thread.join()

print shared_balance

هنا، يمكن مقاطعة التعليمات البرمجية الخاصة بك بين قراءة الحالة المشتركة (balance = shared_balance) وكتابة النتيجة التي تم تغييرها مرة أخرى (shared_balance = balance)، مما تسبب في فقدان التحديث.والنتيجة هي قيمة عشوائية للحالة المشتركة.

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

نصائح أخرى

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

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

إضافة إلى المناقشة:

نظرًا لوجود GIL، فإن بعض العمليات ذرية في Python ولا تحتاج إلى قفل.

http://www.python.org/doc/faq/library/#what-kinds-of-global-value-mutation-are-thread-safe

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

يمنع قفل المترجم العالمي سلاسل الرسائل من الوصول إلى ملف مترجم في وقت واحد (وبالتالي يستخدم CPython نواة واحدة فقط).ومع ذلك، كما أفهمها، لا تزال المواضيع متقطعة ومجدولة بشكل استباقي, ، مما يعني أنك لا تزال بحاجة إلى أقفال على هياكل البيانات المشتركة، خشية أن تدوس خيوطك على أصابع بعضها البعض.

الإجابة التي واجهتها مرارًا وتكرارًا هي أن تعدد العمليات في بايثون نادرًا ما يستحق كل هذا العناء، ولهذا السبب.لقد سمعت أشياء جيدة عن معالجة باي المشروع، الذي يجعل تشغيل عمليات متعددة "بسيطًا" مثل تعدد مؤشرات الترابط، مع هياكل البيانات المشتركة، وقوائم الانتظار، وما إلى ذلك.(سيتم تقديم PyProcessing في المكتبة القياسية لإصدار Python 2.6 القادم باسم معالجة متعددة الوحدة النمطية.) وهذا يجعلك تتعرف على GIL، حيث أن كل عملية لها مترجمها الخاص.

يصف هذا المنشور GIL على مستوى عالٍ إلى حد ما:

ومما يثير الاهتمام بشكل خاص هذه الاقتباسات:

كل عشرة تعليمات (يمكن تغيير هذا الافتراضي) ، يقوم الأساسي بإصدار GIL للخيط الحالي.عند هذه النقطة ، يختار نظام التشغيل سلسلة رسائل من جميع مؤشرات الترابط التي تتنافس على القفل (ربما تختار نفس الخيط الذي أصدر للتو Gil - ليس لديك أي تحكم في الخيط الذي يتم اختياره) ؛هذا الموضوع يكتسب GIL ثم يركض لمدة عشرة أشرطة أخرى.

و

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

يبدو أن GIL يوفر عددًا أقل من الحالات المحتملة لتبديل السياق، ويجعل الأنظمة متعددة النواة/المعالجات تتصرف كنواة واحدة، فيما يتعلق بكل مثيل لمترجم بايثون، لذلك نعم، لا تزال بحاجة إلى استخدام آليات المزامنة.

أعتقد أنه من هذا الطريق:

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

المشكلة هي أنه يمكن تعليق الخيط في أي مكان، على سبيل المثال، إذا كنت أرغب في حساب b = (a + b) * 3، فقد ينتج عن ذلك تعليمات مثل هذا:

1    a += b
2    a *= 3
3    b = a

الآن، لنفترض أن هذا الخيط قيد التشغيل في سلسلة رسائل وتم تعليق هذا الخيط بعد أي من السطر 1 أو 2 ثم يتم تشغيل مؤشر ترابط آخر وتشغيله:

b = 5

ثم عند استئناف مؤشر الترابط الآخر، تتم الكتابة فوق b بواسطة القيم المحسوبة القديمة، وهو على الأرجح ليس ما كان متوقعًا.

لذلك يمكنك أن ترى أنه على الرغم من أنهما لا يعملان فعليًا في نفس الوقت، إلا أنك لا تزال بحاجة إلى القفل.

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

لا تزال هناك حاجة إلى الأقفال.سأحاول شرح سبب الحاجة إليها.

يتم تنفيذ أي عملية/تعليمات في المترجم.يضمن GIL أن يتم الاحتفاظ بالمترجم بواسطة مؤشر ترابط واحد عند لحظة معينة من الزمن.ويعمل برنامجك الذي يحتوي على مؤشرات ترابط متعددة في مترجم واحد.في أي لحظة معينة من الزمن، يتم الاحتفاظ بهذا المترجم بواسطة خيط واحد.وهذا يعني أن الخيط الوحيد الذي يحمل المترجم هو جري في أي لحظة من الزمن.

لنفترض أن هناك خيطين، مثل t1 وt2، وكلاهما يريد تنفيذ تعليمتين تقومان بقراءة قيمة متغير عام وزيادتها.

#increment value
global var
read_var = var
var = read_var + 1

كما هو موضح أعلاه، يضمن GIL فقط عدم تمكن خيطين من تنفيذ التعليمات في وقت واحد، مما يعني أنه لا يمكن تنفيذ كلا الخيطين read_var = var في أي لحظة معينة من الزمن.لكن يمكنهم تنفيذ التعليمات واحدًا تلو الآخر ولا يزال من الممكن أن تواجه مشكلة.خذ بعين الاعتبار هذا الموقف:

  • لنفترض أن read_var هو 0.
  • يتم الاحتفاظ بـ GIL بواسطة الخيط t1.
  • ينفذ t1 read_var = var.لذلك، read_var في t1 هو 0.سيضمن GIL فقط عدم تنفيذ عملية القراءة هذه لأي مؤشر ترابط آخر في هذه اللحظة.
  • يتم إعطاء GIL للخيط t2.
  • ينفذ t2 read_var = var.لكن read_var لا يزال 0.لذلك، read_var في t2 هو 0.
  • يتم إعطاء GIL إلى t1.
  • ينفذ t1 var = read_var+1 ويصبح var 1.
  • يتم إعطاء GIL إلى t2.
  • يعتقد t2 أن read_var=0، لأن هذا هو ما قرأه.
  • ينفذ t2 var = read_var+1 ويصبح var 1.
  • توقعاتنا كانت ذلك var يجب أن تصبح 2.
  • لذلك، يجب استخدام القفل للحفاظ على القراءة والتزايد كعملية ذرية.
  • تشرح إجابة ويل هاريس ذلك من خلال مثال رمزي.

القليل من التحديث من مثال ويل هاريس:

class Withdraw(threading.Thread):  
def run(self):            
    for _ in xrange(1000000):  
        global shared_balance  
        if shared_balance >= 100:
          balance = shared_balance
          balance -= 100  
          shared_balance = balance

ضع بيان التحقق من القيمة في السحب ولم أعد أرى سلبيًا بعد الآن ويبدو أن التحديثات متسقة.سؤالي هو:

إذا كان GIL يمنع تنفيذ مؤشر ترابط واحد فقط في أي وقت ذري، فأين ستكون القيمة التي لا معنى لها؟إذا لم تكن هناك قيمة قديمة، فلماذا نحتاج إلى القفل؟(بافتراض أننا نتحدث فقط عن كود بايثون النقي)

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

مرخصة بموجب: CC-BY-SA مع الإسناد
لا تنتمي إلى StackOverflow
scroll top