python - هل الأقفال غير ضرورية في شفرة بايثون متعددة الخيوط بسبب GIL؟



multithreading locking (9)

إذا كنت تعتمد على تطبيق Python الذي يحتوي على Global Interpreter Lock (أي CPython) وكتابة رمز multithreaded ، هل تحتاج حقًا إلى تأمين على الإطلاق؟

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

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

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


Answers

لا - 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 ) ، مما يتسبب في حدوث تحديث مفقود. والنتيجة هي قيمة عشوائية للحالة المشتركة.

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


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

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

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

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

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

b = 5

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

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


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

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


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


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

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

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

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


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

يتم تنفيذ أي عملية / تعليمات في المترجم. يضمن 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 يمنع يمكن تنفيذ مؤشر ترابط واحد فقط في أي وقت ذري ، فأين سيكون قيمة تالفة؟ إذا لم تكن هناك قيمة قديمة ، فلماذا نحتاج إلى قفل؟ (على افتراض أننا نتحدث فقط عن رمز بيثون نقي)

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


ليس عليك أن تقوم بتغليف الجانب الأيسر في مجموعة. يمكنك فقط القيام بذلك:

if {'foo', 'bar'} <= set(some_dict):
    pass

هذا أيضًا أفضل من حل all(k in d...) .





python multithreading locking