ثلاثة عدّادات تعطي الجواب الصحيح تحت كل الجداول ومع ذلك ليست متماثلة. أوقف خيطًا في أسوأ لحظة فيوقف القفل الدوّار الجميع ويسمح عدّاد المقارنة والتبديل بمرور أحدهم ويتيح عدّاد fetch-and-add لكل خيط أن ينتهي في دور واحد. هذه هي درجات سلّم ضمانات التقدم الثلاث.
خيط واحد متوقف
يستطيع نظام التشغيل الحقيقي أن ينزع خيطًا عن نواته في أي لحظة: تنتهي شريحة وقته أو يرسله خطأ صفحة إلى القرص أو تصل مقاطعة. ينبغي أن تنجو البنية المتزامنة الجيدة من ذلك. نحدد السؤال بخصم: ينفّذ prefix من الأدوار يختاره ثم يوقف خيطًا إلى الأبد ونعطي budget من الجولات لكل الباقين. فإن بقي خيط غير موقوف لم ينتهِ فيمكن إيقاف البنية. يفشل القفل الدوّار في هذا الاختبار بأبسط طريقة: إن أوقف الخصم الخيط الذي يمسك القفل دار الآخرون حتى ينتهي الميزانية.
def locked_increments(k): def thread(shared): for _ in range(k): while True: yield if not shared.get("lock", 0): shared["lock"] = 1 break yield value = shared.get("x", 0) yield shared["x"] = value + 1 yield shared["lock"] = 0 return threaddef progress_without(make_threads, prefix, paused, budget=50): n = len(make_threads({})) allowed = [tid for tid in range(n) if tid not in paused] _, alive = run_prefix(make_threads, list(prefix) + allowed * budget) return [tid for tid in allowed if tid not in alive]lk = lambda shared: [locked_increments(1)(shared), locked_increments(1)(shared)]print(run_prefix(lk, [0]))print("thread 0 paused holding the lock:", progress_without(lk, [0], [0]))print("thread 0 finished first: ", progress_without(lk, [0, 0, 0, 0], [0]))
({'lock': 1}, [0, 1])
thread 0 paused holding the lock: []
thread 0 finished first: [1]بعد دور واحد يمسك الخيط 0 القفل ('lock': 1). أوقفه هناك فلا ينهي الخيط 1 شيئًا في خمسين جولة: []. دعه ينفّذ أدواره الأربعة أولًا فيتحرر القفل ثانية وينتهي الخيط 1: [1]. الفرق هو مكان ضربة الخصم فقط. يحوّل القفل أي توقف داخل المقطع الحرج إلى توقف للبرنامج كله.
بلا أقفال: دائمًا ينتهي أحدهم
تؤتمت is_blocking الخصم. تجرّب كل بادئة من بضعة أدوار وتوقف كل خيط حي بدوره وتسأل progress_without هل ما زال الباقون ينتهون. وفي القفل الدوّار الجواب نعم بعد دور واحد. أما في عدّاد المقارنة والتبديل فلا أبدًا: الخيط الموقوف بين قراءته وتبديله يترك القيمة دون تغيير فينجح تبديل الخيط الآخر. هذا هو تعريف بلا أقفال: النظام ككل يتقدم دائمًا حتى لو أُوقف خيط إلى الأبد.
def is_blocking(make_threads, depth=3, budget=50): def visit(prefix): _, alive = run_prefix(make_threads, prefix) for paused in alive: finished = progress_without(make_threads, prefix, [paused], budget) if len(finished) < len([tid for tid in range(len(make_threads({}))) if tid != paused]): return True if len(prefix) < depth: return any(visit(prefix + [tid]) for tid in alive) return False return visit([])cs = lambda shared: [cas_increments(1)(shared), cas_increments(1)(shared)]at = lambda shared: [atomic_increments(1)(shared), atomic_increments(1)(shared)]print("spin lock:", is_blocking(lk))print("compare-and-swap:", is_blocking(cs))print("fetch-and-add:", is_blocking(at))
spin lock: True compare-and-swap: False fetch-and-add: False
عدّاد المقارنة والتبديل وعدّاد fetch-and-add كلاهما غير حاجب. وللسلّم درجة ثالثة لأن «بلا أقفال» ما زال يسمح بيوم سيئ لخيط واحد: قد يخسر كل السباقات.
بلا انتظار: الجميع ينتهي
للعملية بلا انتظار حدّ على أدوارها لا يستطيع أي جار تجاوزه. تقيسه worst_own_turns: أكبر عدد أدوار يأخذه خيط عبر كل الجداول الكاملة. ثم تطرح classify السؤال الحاسم. هل يبقى أسوأ حالات الخيط 0 نفسه حين يكون لجاره زيادة واحدة وحين يكون له ثلاث؟ إن بقي فالخيط 0 بلا انتظار. وإن نما فبوسع الجار أن يجعل الخيط 0 يعيد المحاولة أكثر فالبنية بلا أقفال فقط.
def worst_own_turns(make_threads, tid): worst = 0 def visit(prefix): nonlocal worst _, alive = run_prefix(make_threads, prefix) if not alive: worst = max(worst, prefix.count(tid)) return for other in alive: visit(prefix + [other]) visit([]) return worstdef classify(factory, others=(1, 3)): def program(m): return lambda shared: [factory(1)(shared), factory(m)(shared)] if is_blocking(program(others[0])): return "blocking" light, busy = worst_own_turns(program(others[0]), 0), worst_own_turns(program(others[1]), 0) return "wait-free" if light == busy else "lock-free"for neighbour in (1, 2, 3): cas = lambda shared: [cas_increments(1)(shared), cas_increments(neighbour)(shared)] atomic = lambda shared: [atomic_increments(1)(shared), atomic_increments(neighbour)(shared)] print(neighbour, "increments next door: CAS", worst_own_turns(cas, 0), "turns, fetch-and-add", worst_own_turns(atomic, 0))print([classify(f) for f in (locked_increments, cas_increments, atomic_increments)])
1 increments next door: CAS 4 turns, fetch-and-add 1 2 increments next door: CAS 6 turns, fetch-and-add 1 3 increments next door: CAS 8 turns, fetch-and-add 1 ['blocking', 'lock-free', 'wait-free']
أقوى ما في السلّم وأكلفه تصميمًا هو بلا انتظار ويبين fetch-and-add أنه ليس بعيد المنال دائمًا. وبلا أقفال هو الهدف العملي للطوابير والمكدسات. القفل مقبول حيث لا يضرك حامل موقوف لكنه غير مقبول أبدًا على مسار يجب ألا يتوقف.
returns = [float(r) for r in load_bars()["close"].pct_change().dropna()][:6]ups = [sum(1 for r in returns[start::2] if r > 0) for start in (0, 1)]for name, factory in (("locked", locked_increments), ("cas", cas_increments), ("atomic", atomic_increments)): program = lambda shared, f=factory: [f(k)(shared) for k in ups] blocking = is_blocking(program) print(name, classify(factory), blocking, None if blocking else worst_own_turns(program, 0))
locked blocking True None cas lock-free False 8 atomic wait-free False 1
اقرأ الأعمدة: يحتاج خيط fetch-and-add إلى دور واحد مهما انشغل جاره بينما يحتاج خيط المقارنة والتبديل إلى 4 ثم 6 ثم 8 أدوار كلما زادت زيادات الجار. يمكن لكل زيادة إضافية عند الجار أن تجعل الخيط 0 يخسر سباقًا آخر ويكرر قراءته وتبديله. هذا النمو هو بالضبط ما يفصل بلا أقفال عن بلا انتظار.
حاجب: خيط موقوف يستطيع إيقاف الباقين. بلا أقفال: ينتهي خيط ما دائمًا. بلا انتظار: ينتهي كل خيط في عدد محدود من أدواره. كل مستوى يستلزم الذي قبله في هذه القائمة.
ما ستبنيه
يتبع المختبر الدرس: عدّاد القفل الدوّار والتشغيل مع إيقاف بعض الخيوط والاختبار الذي يقول هل يمكن لإيقاف خيط أن يوقف الباقين وأسوأ حالات أدوار الخيط الخاصة وتصنيف عدّاد ودراسة العدّادات الثلاثة على أيام صعود SPY.
- الدالة · السؤالتعتمد على
- locked_increments(k)عدّاد ينتظر قفلًا؟حساب بسيط
- progress_without(make_threads, prefix, paused, budget)من ينتهي إن أُوقفت بعض الخيوط؟run_prefix
- is_blocking(make_threads, depth, budget)هل يستطيع خيط موقوف إيقاف الجميع؟progress_without
- worst_own_turns(make_threads, tid)كم دورًا قد يحتاج خيط؟run_prefix
- classify(factory, others)حاجب أم بلا أقفال أم بلا انتظار؟is_blockingworst_own_turns
- study(bars)أين تقف العدّادات الثلاثة على SPY؟classifyis_blockingworst_own_turns
الموضوع التالي: تعرف الآن ما يمكن أن يعد به طابور. مخزن حلقي لمنتج واحد ومستهلك واحد يبني أبسط طابور مفيد بلا أقفال بكاتب واحد وقارئ واحد وذاكرة ثابتة ودون أي قراءة وتعديل وكتابة ذرّية ويتحقق المستكشف أنه لا يضيّع تيكًا ولا يعيد ترتيبه أبدًا.
المختبر: ما الذي يُقيَّم
6 مهام · 100 نقطة- عدّاد على قفل دوّار10 نقطة
- التشغيل مع خيوط موقوفة20 نقطة
- هل يوقف توقف واحد الجميع20 نقطة
- أسوأ حالة لخيط15 نقطة
- تسمية الضمان10 نقطة
- الدراسة على جدول SPY25 نقطة