مع منتج واحد ومستهلك واحد يكون لكل موضع كاتب واحد. ومع الكثيرين يجب توزيع المواضع ذرّيًا ولا تكفي التذكرة: المنتج الذي يحمل الموضع 5 لم يكتب عنصره بعد. يقول رقم تسلسل على كل خانة لمن دورها. وعلى 2756 جدولًا لمنتجين و1600 لمستهلكين لا ينكسر هذا الطابور أبدًا.
التذكرة ليست تسليمًا
يستعمل الطابور المحدود ذو capacity خانة عدّادين لا يزيدان إلا. يأخذ المنتج الموضع التالي بمقارنة وتبديل على enq ويأخذ المستهلك التالي بمقارنة وتبديل على deq. يقع الموضع p في الخانة p % capacity. تجعل المقارنة والتبديل الحجز آمنًا: لا يحصل منتجان على الموضع نفسه. لكن حجز موضع ليس كتابة العنصر. بين التبديل والتخزين تكون الخانة محجوزة وفارغة ولو قرأها مستهلك الآن لقرأ هراءً. لذا تحمل كل خانة رقم تسلسل لا يتقدم إلا في اللحظة الصحيحة.
def init_queue(capacity): shared = {"enq": 0, "deq": 0} for cell in range(capacity): shared["seq%d" % cell] = cell return shareddef enqueue(shared, item, capacity): while True: yield pos = shared["enq"] cell = pos % capacity diff = shared["seq%d" % cell] - pos if diff < 0: return False if diff > 0: continue yield if compare_and_swap(shared, "enq", pos, pos + 1): break yield shared["data%d" % cell] = item shared["seq%d" % cell] = pos + 1 shared["put%d" % pos] = item return Truedef dequeue(shared, capacity): while True: yield pos = shared["deq"] cell = pos % capacity diff = shared["seq%d" % cell] - (pos + 1) if diff < 0: return None if diff > 0: continue yield if compare_and_swap(shared, "deq", pos, pos + 1): break yield item = shared.get("data%d" % cell) shared["seq%d" % cell] = pos + capacity shared["got%d" % pos] = item return itemsolo = lambda shared: [producer_thread((7,), 2)(shared), consumer_thread(1, 2)(shared)]for prefix in ([0, 0, 0], [0, 0, 0, 1, 1, 1], [0, 0, 1, 1, 0, 1, 1]): print(prefix, run_prefix(solo, prefix, init_queue(2))[0])
[0, 0, 0] {'enq': 1, 'deq': 0, 'seq0': 1, 'seq1': 1, 'data0': 7, 'put0': 7}
[0, 0, 0, 1, 1, 1] {'enq': 1, 'deq': 1, 'seq0': 2, 'seq1': 1, 'data0': 7, 'put0': 7, 'got0': 7}
[0, 0, 1, 1, 0, 1, 1] {'enq': 1, 'deq': 0, 'seq0': 1, 'seq1': 1, 'data0': 7, 'put0': 7}اقرأ التشغيلات الثلاثة. بعد [0, 0, 0] حجز المنتج الموضع 0 (enq: 1) وخزّن data0 ورفع seq0 إلى 1 فالعنصر جاهز. وبعد أدوار المستهلك الثلاثة يصير got0: 7 وseq0: 2: الخانة حرة للدورة التالية. وفي التشغيل الأخير ينظر المستهلك بين حجز المنتج ونشره فيرى seq0 = 0 بدل 1 فيمضي خالي اليدين. لا يقرأ هراءً. يقول فقط «لا شيء جاهز بعد» وستجد محاولة لاحقة العنصر.
فحص كل الجداول
ماذا يجب أن يعد الطابور الآن؟ يسجّل المنتجون كل عنصر تحت موضعه (put3) ويسجّل المستهلكون ما أخذوه تحت الموضع الذي حجزوه (got3). إن كان الطابور يعمل فكل موضع سُحب يحمل العنصر الذي وُضع فيه (لا mismatch) والمواضع المسحوبة هي 0 و1 و2 ... بلا ثقوب ولا تكرار (لا gap). وهذا يعطي الطابور ترتيبه: ترتيب المواضع. تقرأ violations هاتين الحقيقتين من حالة نهائية وتشغّل verify المستكشف لأي عدد من المنتجين والمستهلكين. هنا منتج واحد بعنصرين أمام مستهلكين اثنين وعنصر واحد أمام مستهلك واحد.
def violations(final): puts = {int(key[3:]): value for key, value in final.items() if key.startswith("put")} gots = {int(key[3:]): value for key, value in final.items() if key.startswith("got")} broken = [] if any(gots[p] != puts.get(p) for p in gots): broken.append("mismatch") if sorted(gots) != list(range(len(gots))): broken.append("gap") return sorted(broken)def verify(producers, consumers, capacity): def make_threads(shared): return [producer_thread(items, capacity)(shared) for items in producers] + [consumer_thread(n, capacity)(shared) for n in consumers] outcomes = explore_all(make_threads, init_queue(capacity)) broken = {} pushed = popped = 0 for key, count in outcomes.items(): final = dict(key) pushed = max(pushed, sum(1 for k in final if k.startswith("put"))) popped = max(popped, sum(1 for k in final if k.startswith("got"))) for name in violations(final): broken[name] = broken.get(name, 0) + count return {"schedules": sum(outcomes.values()), "broken": dict(sorted(broken.items())), "pushed": pushed, "popped": popped}print(verify([(7,)], [1], 2))print(verify([(7, 8)], [1, 1], 2))
{'schedules': 4, 'broken': {}, 'pushed': 1, 'popped': 1}
{'schedules': 1600, 'broken': {}, 'pushed': 2, 'popped': 2}عدّاد على كل خانة يسجّل لمن الدور. لا يتغير إلا حين يُكتب العنصر (إلى p + 1) وحين يُؤخذ (إلى p + capacity) فيعرف الخيط الذي يقرؤه هل العنصر آمن الاستعمال دون أي قفل.
ما الذي يشتريه رقم التسلسل
لنرَ قيمته بإزالته. يحجز المستهلك المهمل أدناه موضعًا بالمقارنة والتبديل ثم يقرأ الخانة فورًا دون أن ينظر إلى seq. أمام منتج بعنصر واحد له 20 جدولًا وفي 10 منها يحجز موضعًا قبل أن ينشر المنتج شيئًا ويعيد None كعنصر أو العنصر الخطأ. أما الطابور الحقيقي فله 4 جداول للبرنامج نفسه ولا ينكسر أيٌّ منها. رقم التسلسل هو بالضبط الفحص الذي يحوّل تذكرة محجوزة إلى عنصر مسلَّم.
def careless_dequeue(shared, capacity): yield pos = shared["deq"] cell = pos % capacity yield if not compare_and_swap(shared, "deq", pos, pos + 1): return None yield item = shared.get("data%d" % cell) shared["got%d" % pos] = item return itemdef careless_consumer(attempts, capacity): def thread(shared): for _ in range(attempts): yield from careless_dequeue(shared, capacity) return threadmake = lambda shared: [producer_thread((7,), 2)(shared), careless_consumer(1, 2)(shared)]outcomes = explore_all(make, init_queue(2))broken = sum(n for key, n in outcomes.items() if violations(dict(key)))print(sum(outcomes.values()), "schedules,", broken, "break the invariants")
20 schedules, 10 break the invariants
مع خانة واحدة تكون القيمة p + 1 التي تعني «جاهزة للموضع p» هي نفسها التي تعني «حرة للموضع p + 1» وقد يكتب منتج فوق عنصر لم يُقرأ. ولهذا تقرّب التطبيقات الحقيقية السعة إلى قوة من اثنين لا تقل عن 2 وهذا يحوّل % capacity أيضًا إلى قناع رخيص.
ticks = [int(round(float(c) * 100)) for c in load_bars()["close"].iloc[:2]]print(ticks)print("two producers:", verify([(ticks[0],), (ticks[1],)], [2], 2))print("two consumers:", verify([tuple(ticks)], [1, 1], 2))
[9369, 9305]
two producers: {'schedules': 2756, 'broken': {}, 'pushed': 2, 'popped': 2}
two consumers: {'schedules': 1600, 'broken': {}, 'pushed': 2, 'popped': 2}كلا شكلي الطابور صحيح على كل الجداول والمستكشف شاهد بينما تفشل نسخة بلا فحص التسلسل في نصف فضائها (الأصغر بكثير). لا ينتظر المنتجون والمستهلكون بعضهم أبدًا: المستهلك الذي لا يجد شيئًا جاهزًا يعود ببساطة والمنتج الذي يجد الطابور ممتلئًا يعيد False. هذا هو ثمن الطابور بلا أقفال وهديته: قد تفشل العمليات لكنها لا تحجب أبدًا.
ما ستبنيه
يتبع المختبر الدرس: الطابور الفارغ بأرقام تسلسله وenqueue وdequeue بحجز بالمقارنة والتبديل ونشر وفحص الثابتين والمدقق الذي يشغّل المستكشف لأي عدد من المنتجين والمستهلكين ودراسة على إغلاقين لـSPY.
- الدالة · السؤالتعتمد على
- init_queue(capacity)كيف يبدو طابور فارغ؟حساب بسيط
- enqueue(shared, item, capacity)كيف يحجز المنتج وينشر؟حساب بسيط
- dequeue(shared, capacity)كيف يحجز المستهلك ويحرّر؟حساب بسيط
- violations(final)أي الثوابت تكسرها حالة نهائية؟حساب بسيط
- verify(producers, consumers, capacity)هل الطابور صحيح تحت كل الجداول؟explore_allviolations
- study(bars)هل يصمد الشكلان على إغلاقات SPY؟verify
الموضوع التالي: أثبتّ الآن صحة برامج صغيرة بتشغيل كل الجداول. اختبار الشيفرة المتزامنة يقارن ذلك بالأدوات التي تستعملها الفرق الحقيقية: كاشف تسابق يراقب تشغيلًا واحدًا ويشير إلى الوصولات التي قد تتسابق، ومدقق نماذج يفعل ما يفعله مستكشفك لكن بالحيل التي تتيح له التوسع.
المختبر: ما الذي يُقيَّم
6 مهام · 100 نقطة- الطابور الفارغ5 نقطة
- الإدخال25 نقطة
- السحب25 نقطة
- الثوابت10 نقطة
- التحقق على كل الجداول15 نقطة
- الدراسة على جدول SPY20 نقطة