هل يعتبر نهج بايثون المنتج والمستهلك بدون قفل آمنًا؟

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

سؤال

لقد كتبت مؤخرًا برنامجًا يستخدم نمط المنتج/المستهلك البسيط.كان به في البداية خطأ يتعلق بالاستخدام غير السليم لمؤشر الترابط. تم إصلاح القفل في النهاية.لكنه جعلني أفكر فيما إذا كان من الممكن تنفيذ نمط المنتج/المستهلك بطريقة غير مقفلة.

المتطلبات في حالتي كانت بسيطة:

  • موضوع منتج واحد.
  • موضوع المستهلك واحد.
  • قائمة الانتظار لها مكان لعنصر واحد فقط.
  • يمكن للمنتج إنتاج العنصر التالي قبل استهلاك العنصر الحالي.وبالتالي فإن العنصر الحالي مفقود، ولكن هذا جيد بالنسبة لي.
  • يمكن للمستهلك استهلاك العنصر الحالي قبل إنتاج العنصر التالي.وبالتالي يتم استهلاك العنصر الحالي مرتين (أو أكثر)، ولكن هذا جيد بالنسبة لي.

لذلك كتبت هذا:

QUEUE_ITEM = None

# this is executed in one threading.Thread object
def producer():
    global QUEUE_ITEM
    while True:
        i = produce_item()
        QUEUE_ITEM = i

# this is executed in another threading.Thread object
def consumer():
    global QUEUE_ITEM
    while True:
        i = QUEUE_ITEM
        consume_item(i)

سؤالي هو:هل هذا الكود آمن للخيط؟

تعليق فوري:هذا الرمز ليس غير قابل للقفل حقًا - أنا أستخدم CPython وهو يحتوي على GIL.

لقد اختبرت الكود قليلاً ويبدو أنه يعمل.إنه يترجم إلى بعض عمليات LOAD و STORE التي تكون ذرية بسبب GIL.لكنني أعرف ذلك أيضًا del x العملية ليست ذرية عند تنفيذ x __del__ طريقة.لذا، إذا كان العنصر الخاص بي يحتوي على ملف __del__ الطريقة وتحدث بعض الجدولة السيئة، قد تنكسر الأمور.أم لا؟

سؤال آخر هو:ما نوع القيود (على سبيل المثال، نوع العناصر المنتجة) التي يجب أن أفرضها لجعل الكود أعلاه يعمل بشكل جيد؟

أسئلتي تتعلق فقط بالإمكانية النظرية لاستغلال مراوغات CPython وGIL من أجل التوصل إلى نظام غير قابل للقفل (أي.لا يوجد أقفال مثل Threading.Lock بشكل صريح في الكود).

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

المحلول

نعم سيعمل هذا بالطريقة التي وصفتها:

  1. أنه يجوز للمنتج إنتاج عنصر قابل للتخطي.
  2. أن المستهلك قد يستهلك نفس العنصر.

لكنني أعلم أيضًا أن عملية del x ليست ذرية عند تنفيذ x ديل طريقة.لذا، إذا كان العنصر الخاص بي يحتوي على ملف ديل الطريقة وتحدث بعض الجدولة السيئة، قد تنكسر الأمور.

لا أرى "ديل" هنا.إذا حدث ديل في consum_item فإن ديل قد تحدث في موضوع المنتج.لا أعتقد أن هذا سيكون "مشكلة".

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

نصائح أخرى

الخداع سوف يعضك.مجرد استخدام قائمة الانتظار للتواصل بين المواضيع.

هذا ليس حقًا الخيط آمن لأن المنتج يمكنه الكتابة فوقه QUEUE_ITEM قبل أن يستهلكها المستهلك ويمكن للمستهلك أن يستهلكها QUEUE_ITEM مرتين.كما ذكرت، أنت موافق على ذلك ولكن معظم الناس ليسوا كذلك.

سيتعين على أي شخص لديه معرفة أكبر بالأجزاء الداخلية لـ cpython أن يجيبك على المزيد من الأسئلة النظرية.

أعتقد أنه من الممكن أن تتم مقاطعة سلسلة المحادثات أثناء الإنتاج/الاستهلاك، خاصة إذا كانت العناصر كائنات كبيرة.يحرر:هذا مجرد تخمين جامح.أنا لست خبيرا.

كما قد تنتج/تستهلك الخيوط أي عدد من العناصر قبل بدء تشغيل العنصر الآخر.

يمكنك استخدام قائمة كقائمة انتظار طالما أنك تلتزم بالإلحاق/البوب ​​لأن كلاهما ذري.

QUEUE = []

# this is executed in one threading.Thread object
def producer():
    global QUEUE
    while True:
        i = produce_item()
        QUEUE.append(i)

# this is executed in another threading.Thread object
def consumer():
    global QUEUE
    while True:
        try:
            i = QUEUE.pop(0)
        except IndexError:
            # queue is empty
            continue

        consume_item(i)

في نطاق فئة مثل أدناه، يمكنك حتى مسح قائمة الانتظار.

class Atomic(object):
    def __init__(self):
        self.queue = []

    # this is executed in one threading.Thread object
    def producer(self):
        while True:
            i = produce_item()
            self.queue.append(i)

    # this is executed in another threading.Thread object
    def consumer(self):
        while True:
            try:
                i = self.queue.pop(0)
            except IndexError:
                # queue is empty
                continue

            consume_item(i)

    # There's the possibility producer is still working on it's current item.
    def clear_queue(self):
        self.queue = []

سيتعين عليك معرفة عمليات القائمة التي تعتبر ذرية من خلال النظر إلى الكود الثانوي الذي تم إنشاؤه.

ال __del__ يمكن أن تكون مشكلة كما قلت.يمكن تجنب ذلك، فقط إذا كانت هناك طريقة لمنع جامع البيانات المهملة من استدعاء ملف __del__ الطريقة على الكائن القديم قبل أن ننتهي من تعيين الطريقة الجديدة للكائن QUEUE_ITEM.سنحتاج إلى شيء مثل:

increase the reference counter on the old object
assign a new one to `QUEUE_ITEM`
decrease the reference counter on the old object

أخشى أنني لا أعرف إذا كان ذلك ممكنًا أم لا.

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