[{"data":1,"prerenderedAt":1381},["ShallowReactive",2],{"page-\u002Ftesting-async-and-concurrent-python\u002Ftesting-threads-and-race-conditions\u002Ftesting-thread-safety-with-barriers-and-events\u002F":3},{"id":4,"title":5,"body":6,"description":1344,"extension":1345,"meta":1346,"navigation":99,"path":1377,"seo":1378,"stem":1379,"__hash__":1380},"content\u002Ftesting-async-and-concurrent-python\u002Ftesting-threads-and-race-conditions\u002Ftesting-thread-safety-with-barriers-and-events\u002Findex.md","Testing Thread Safety with Barriers and Events",{"type":7,"value":8,"toc":1333},"minimark",[9,13,32,37,70,74,77,269,412,586,590,600,616,620,695,699,709,794,817,820,830,896,900,905,1048,1051,1058,1062,1071,1174,1191,1248,1252,1264,1270,1292,1296,1324,1329],[10,11,12],"p",{},"The difference between a concurrency test that is useful and one that is theatre is whether the threads were actually contending. A test that starts eight threads and asserts a total is measuring the scheduler's mood: on an idle machine they run almost serially, the assertion passes, and the bug ships. Coordination primitives fix that by making the contention a precondition rather than a hope.",[10,14,15,16,20,21,20,24,27,28,31],{},"This guide covers the four primitives worth knowing for test code — ",[17,18,19],"code",{},"Barrier",", ",[17,22,23],{},"Event",[17,25,26],{},"Semaphore"," and ",[17,29,30],{},"Condition"," — with the coordination shape each one fits and the failure modes each one introduces when misused.",[33,34,36],"h2",{"id":35},"prerequisites","Prerequisites",[38,39,40,48,58,67],"ul",{},[41,42,43,44,47],"li",{},"Python 3.9+; everything here is ",[17,45,46],{},"threading"," from the standard library.",[41,49,50,53,54,57],{},[17,51,52],{},"pytest >= 8.0"," with ",[17,55,56],{},"pytest-timeout",", so a coordination mistake fails the test instead of hanging the suite.",[41,59,60,61,66],{},"The reasoning behind deterministic reproduction in ",[62,63,65],"a",{"href":64},"\u002Ftesting-async-and-concurrent-python\u002Ftesting-threads-and-race-conditions\u002F","testing threads and race conditions",".",[41,68,69],{},"An invariant worth asserting — a total, a count, a uniqueness property — because none of this is useful without one.",[33,71,73],{"id":72},"solution","Solution",[10,75,76],{},"Match the primitive to the shape of the coordination, and give every wait a timeout.",[78,79,84],"pre",{"className":80,"code":81,"language":82,"meta":83,"style":83},"language-python shiki shiki-themes github-light github-dark","import threading\n\nimport pytest\n\n\n@pytest.mark.timeout(10)\ndef test_counter_is_atomic_under_contention():\n    counter = Counter()\n    workers = 8\n    # Barrier: nobody proceeds until all eight have arrived, so the increment\n    # loops overlap rather than running one after another.\n    gate = threading.Barrier(workers, timeout=5)\n    errors: list[BaseException] = []\n\n    def worker():\n        try:\n            gate.wait()\n            for _ in range(2_000):\n                counter.increment()\n        except BaseException as exc:      # noqa: BLE001 — re-raised below\n            errors.append(exc)\n\n    threads = [threading.Thread(target=worker) for _ in range(workers)]\n    for thread in threads:\n        thread.start()\n    for thread in threads:\n        thread.join(timeout=5)\n        assert not thread.is_alive(), \"a worker never finished\"\n\n    assert not errors, errors[0]\n    assert counter.value == workers * 2_000\n","python","",[17,85,86,94,101,107,112,117,123,129,135,141,147,153,159,165,170,176,182,188,194,200,206,212,217,223,229,235,240,246,252,257,263],{"__ignoreMap":83},[87,88,91],"span",{"class":89,"line":90},"line",1,[87,92,93],{},"import threading\n",[87,95,97],{"class":89,"line":96},2,[87,98,100],{"emptyLinePlaceholder":99},true,"\n",[87,102,104],{"class":89,"line":103},3,[87,105,106],{},"import pytest\n",[87,108,110],{"class":89,"line":109},4,[87,111,100],{"emptyLinePlaceholder":99},[87,113,115],{"class":89,"line":114},5,[87,116,100],{"emptyLinePlaceholder":99},[87,118,120],{"class":89,"line":119},6,[87,121,122],{},"@pytest.mark.timeout(10)\n",[87,124,126],{"class":89,"line":125},7,[87,127,128],{},"def test_counter_is_atomic_under_contention():\n",[87,130,132],{"class":89,"line":131},8,[87,133,134],{},"    counter = Counter()\n",[87,136,138],{"class":89,"line":137},9,[87,139,140],{},"    workers = 8\n",[87,142,144],{"class":89,"line":143},10,[87,145,146],{},"    # Barrier: nobody proceeds until all eight have arrived, so the increment\n",[87,148,150],{"class":89,"line":149},11,[87,151,152],{},"    # loops overlap rather than running one after another.\n",[87,154,156],{"class":89,"line":155},12,[87,157,158],{},"    gate = threading.Barrier(workers, timeout=5)\n",[87,160,162],{"class":89,"line":161},13,[87,163,164],{},"    errors: list[BaseException] = []\n",[87,166,168],{"class":89,"line":167},14,[87,169,100],{"emptyLinePlaceholder":99},[87,171,173],{"class":89,"line":172},15,[87,174,175],{},"    def worker():\n",[87,177,179],{"class":89,"line":178},16,[87,180,181],{},"        try:\n",[87,183,185],{"class":89,"line":184},17,[87,186,187],{},"            gate.wait()\n",[87,189,191],{"class":89,"line":190},18,[87,192,193],{},"            for _ in range(2_000):\n",[87,195,197],{"class":89,"line":196},19,[87,198,199],{},"                counter.increment()\n",[87,201,203],{"class":89,"line":202},20,[87,204,205],{},"        except BaseException as exc:      # noqa: BLE001 — re-raised below\n",[87,207,209],{"class":89,"line":208},21,[87,210,211],{},"            errors.append(exc)\n",[87,213,215],{"class":89,"line":214},22,[87,216,100],{"emptyLinePlaceholder":99},[87,218,220],{"class":89,"line":219},23,[87,221,222],{},"    threads = [threading.Thread(target=worker) for _ in range(workers)]\n",[87,224,226],{"class":89,"line":225},24,[87,227,228],{},"    for thread in threads:\n",[87,230,232],{"class":89,"line":231},25,[87,233,234],{},"        thread.start()\n",[87,236,238],{"class":89,"line":237},26,[87,239,228],{},[87,241,243],{"class":89,"line":242},27,[87,244,245],{},"        thread.join(timeout=5)\n",[87,247,249],{"class":89,"line":248},28,[87,250,251],{},"        assert not thread.is_alive(), \"a worker never finished\"\n",[87,253,255],{"class":89,"line":254},29,[87,256,100],{"emptyLinePlaceholder":99},[87,258,260],{"class":89,"line":259},30,[87,261,262],{},"    assert not errors, errors[0]\n",[87,264,266],{"class":89,"line":265},31,[87,267,268],{},"    assert counter.value == workers * 2_000\n",[78,270,272],{"className":80,"code":271,"language":82,"meta":83,"style":83},"import threading\n\n\ndef test_consumer_sees_every_item():\n    produced = threading.Event()          # Event: one signal, many listeners\n    queue: list[int] = []\n    lock = threading.Lock()\n\n    def producer():\n        with lock:\n            queue.extend(range(100))\n        produced.set()                    # tell everyone the data is ready\n\n    def consumer(seen):\n        assert produced.wait(timeout=5), \"producer never signalled\"\n        with lock:\n            seen.extend(queue)\n\n    seen_a, seen_b = [], []\n    threads = [\n        threading.Thread(target=producer),\n        threading.Thread(target=consumer, args=(seen_a,)),\n        threading.Thread(target=consumer, args=(seen_b,)),\n    ]\n    for thread in threads:\n        thread.start()\n    for thread in threads:\n        thread.join(timeout=5)\n\n    assert seen_a == seen_b == list(range(100))\n",[17,273,274,278,282,286,291,296,301,306,310,315,320,325,330,334,339,344,348,353,357,362,367,372,377,382,387,391,395,399,403,407],{"__ignoreMap":83},[87,275,276],{"class":89,"line":90},[87,277,93],{},[87,279,280],{"class":89,"line":96},[87,281,100],{"emptyLinePlaceholder":99},[87,283,284],{"class":89,"line":103},[87,285,100],{"emptyLinePlaceholder":99},[87,287,288],{"class":89,"line":109},[87,289,290],{},"def test_consumer_sees_every_item():\n",[87,292,293],{"class":89,"line":114},[87,294,295],{},"    produced = threading.Event()          # Event: one signal, many listeners\n",[87,297,298],{"class":89,"line":119},[87,299,300],{},"    queue: list[int] = []\n",[87,302,303],{"class":89,"line":125},[87,304,305],{},"    lock = threading.Lock()\n",[87,307,308],{"class":89,"line":131},[87,309,100],{"emptyLinePlaceholder":99},[87,311,312],{"class":89,"line":137},[87,313,314],{},"    def producer():\n",[87,316,317],{"class":89,"line":143},[87,318,319],{},"        with lock:\n",[87,321,322],{"class":89,"line":149},[87,323,324],{},"            queue.extend(range(100))\n",[87,326,327],{"class":89,"line":155},[87,328,329],{},"        produced.set()                    # tell everyone the data is ready\n",[87,331,332],{"class":89,"line":161},[87,333,100],{"emptyLinePlaceholder":99},[87,335,336],{"class":89,"line":167},[87,337,338],{},"    def consumer(seen):\n",[87,340,341],{"class":89,"line":172},[87,342,343],{},"        assert produced.wait(timeout=5), \"producer never signalled\"\n",[87,345,346],{"class":89,"line":178},[87,347,319],{},[87,349,350],{"class":89,"line":184},[87,351,352],{},"            seen.extend(queue)\n",[87,354,355],{"class":89,"line":190},[87,356,100],{"emptyLinePlaceholder":99},[87,358,359],{"class":89,"line":196},[87,360,361],{},"    seen_a, seen_b = [], []\n",[87,363,364],{"class":89,"line":202},[87,365,366],{},"    threads = [\n",[87,368,369],{"class":89,"line":208},[87,370,371],{},"        threading.Thread(target=producer),\n",[87,373,374],{"class":89,"line":214},[87,375,376],{},"        threading.Thread(target=consumer, args=(seen_a,)),\n",[87,378,379],{"class":89,"line":219},[87,380,381],{},"        threading.Thread(target=consumer, args=(seen_b,)),\n",[87,383,384],{"class":89,"line":225},[87,385,386],{},"    ]\n",[87,388,389],{"class":89,"line":231},[87,390,228],{},[87,392,393],{"class":89,"line":237},[87,394,234],{},[87,396,397],{"class":89,"line":242},[87,398,228],{},[87,400,401],{"class":89,"line":248},[87,402,245],{},[87,404,405],{"class":89,"line":254},[87,406,100],{"emptyLinePlaceholder":99},[87,408,409],{"class":89,"line":259},[87,410,411],{},"    assert seen_a == seen_b == list(range(100))\n",[413,414,417,582],"figure",{"className":415},[416],"diagram",[418,419,426,427,426,431,426,435,426,443,426,453,426,462,426,465,426,470,426,476,426,480,426,485,426,489,426,492,426,497,426,500,426,502,426,506,426,510,426,513,426,515,426,518,426,521,426,524,426,528,426,530,426,534,426,538,426,541,426,544,426,547,426,550,426,553,426,558,426,560,426,564,426,568,426,571,426,573,426,576,426,579],"svg",{"viewBox":420,"role":421,"ariaLabelledBy":422,"xmlns":425},"0 0 820 272","img",[423,424],"prim-t","prim-d","http:\u002F\u002Fwww.w3.org\u002F2000\u002Fsvg","\n  ",[428,429,430],"title",{"id":423},"Four coordination primitives and the shapes they fit",[432,433,434],"desc",{"id":424},"Four cards. A barrier holds N peers until all arrive, then releases them together. An event lets one thread signal many waiters once. A semaphore bounds how many threads hold a resource at once. A condition wakes waiters when a predicate over shared state becomes true. Each card names the test scenario it suits.",[436,437],"rect",{"x":438,"y":438,"width":439,"height":440,"rx":441,"fill":442},"0","820","272","14","#fffdf8",[444,445,452],"text",{"x":446,"y":447,"textAnchor":448,"fontSize":449,"fontWeight":450,"fill":451},"410","28","middle","16","700","#3d405b","Pick the primitive from the coordination, not from habit",[436,454],{"x":455,"y":456,"width":457,"height":458,"rx":459,"fill":442,"stroke":460,"strokeWidth":461},"24","52","188","196","12","#81b29a","2",[436,463],{"x":455,"y":456,"width":457,"height":464,"rx":459,"fill":451},"30",[444,466,469],{"x":467,"y":468,"textAnchor":448,"fontSize":459,"fontWeight":450,"fill":442},"118","72","Barrier(n)",[444,471,475],{"x":472,"y":473,"fontSize":474,"fill":451},"38","106","11","n peers arrive,",[444,477,479],{"x":472,"y":478,"fontSize":474,"fill":451},"126","all released together",[444,481,484],{"x":472,"y":482,"fontSize":474,"fontWeight":450,"fill":483},"156","#2a5f49","use for:",[444,486,488],{"x":472,"y":487,"fontSize":474,"fill":451},"176","contention between",[444,490,491],{"x":472,"y":458,"fontSize":474,"fill":451},"identical operations",[444,493,496],{"x":472,"y":494,"fontSize":474,"fill":495},"226","#8f3d22","breaks if one crashes",[436,498],{"x":499,"y":456,"width":457,"height":458,"rx":459,"fill":442,"stroke":460,"strokeWidth":461},"222",[436,501],{"x":499,"y":456,"width":457,"height":464,"rx":459,"fill":451},[444,503,505],{"x":504,"y":468,"textAnchor":448,"fontSize":459,"fontWeight":450,"fill":442},"316","Event()",[444,507,509],{"x":508,"y":473,"fontSize":474,"fill":451},"236","one set(), many",[444,511,512],{"x":508,"y":478,"fontSize":474,"fill":451},"waiters released",[444,514,484],{"x":508,"y":482,"fontSize":474,"fontWeight":450,"fill":483},[444,516,517],{"x":508,"y":487,"fontSize":474,"fill":451},"\"I am inside the",[444,519,520],{"x":508,"y":458,"fontSize":474,"fill":451},"window now\"",[444,522,523],{"x":508,"y":494,"fontSize":474,"fill":451},"latching: stays set",[436,525],{"x":526,"y":456,"width":457,"height":458,"rx":459,"fill":442,"stroke":527,"strokeWidth":461},"420","#f2cc8f",[436,529],{"x":526,"y":456,"width":457,"height":464,"rx":459,"fill":451},[444,531,533],{"x":532,"y":468,"textAnchor":448,"fontSize":459,"fontWeight":450,"fill":442},"514","Semaphore(k)",[444,535,537],{"x":536,"y":473,"fontSize":474,"fill":451},"434","at most k holders",[444,539,540],{"x":536,"y":478,"fontSize":474,"fill":451},"at any moment",[444,542,484],{"x":536,"y":482,"fontSize":474,"fontWeight":450,"fill":543},"#8a5a00",[444,545,546],{"x":536,"y":487,"fontSize":474,"fill":451},"asserting a pool's",[444,548,549],{"x":536,"y":458,"fontSize":474,"fill":451},"concurrency limit",[444,551,552],{"x":536,"y":494,"fontSize":474,"fill":451},"counts, not signals",[436,554],{"x":555,"y":456,"width":556,"height":458,"rx":459,"fill":442,"stroke":557,"strokeWidth":461},"618","178","#e07a5f",[436,559],{"x":555,"y":456,"width":556,"height":464,"rx":459,"fill":451},[444,561,563],{"x":562,"y":468,"textAnchor":448,"fontSize":459,"fontWeight":450,"fill":442},"707","Condition()",[444,565,567],{"x":566,"y":473,"fontSize":474,"fill":451},"632","wait until a",[444,569,570],{"x":566,"y":478,"fontSize":474,"fill":451},"predicate holds",[444,572,484],{"x":566,"y":482,"fontSize":474,"fontWeight":450,"fill":495},[444,574,575],{"x":566,"y":487,"fontSize":474,"fill":451},"state-dependent",[444,577,578],{"x":566,"y":458,"fontSize":474,"fill":451},"handoffs",[444,580,581],{"x":566,"y":494,"fontSize":474,"fill":451},"always loop on wait",[583,584,585],"figcaption",{},"Barriers coordinate peers, events coordinate a signaller and listeners, semaphores bound occupancy, and conditions wait on state. Using the wrong one produces coordination that works by accident.",[33,587,589],{"id":588},"why-this-works","Why this works",[10,591,592,593,595,596,599],{},"A ",[17,594,19],{}," blocks every caller of ",[17,597,598],{},"wait()"," until the configured number have arrived, then releases them all. In a test that means the increment loops start simultaneously rather than in start-up order, which is what produces genuine overlap on a multi-core machine. Without it, thread one typically finishes its loop before thread eight has been scheduled at all, and the lost-update window is never entered.",[10,601,602,603,605,606,608,609,611,612,615],{},"An ",[17,604,23],{}," is a latch: once set, every current and future ",[17,607,598],{}," returns immediately. That asymmetry is exactly right for \"the producer has reached the point I care about\" and exactly wrong for a repeated handoff, where the second iteration finds the event already set. For repeated signalling, a ",[17,610,30],{}," with an explicit predicate, or a ",[17,613,614],{},"queue.Queue",", is the correct primitive.",[33,617,619],{"id":618},"edge-cases-and-failure-modes","Edge cases and failure modes",[38,621,622,633,646,659,681],{},[41,623,624,628,629,632],{},[625,626,627],"strong",{},"A barrier participant that raises before arriving."," The barrier breaks and every other waiter gets ",[17,630,631],{},"BrokenBarrierError",", masking the original exception. Wrap worker bodies so the real error is recorded and reported.",[41,634,635,638,639,642,643,645],{},[625,636,637],{},"An event used for repeated handoffs."," The second wait returns instantly because the event is still set. ",[17,640,641],{},"clear()"," between iterations reintroduces a race; use a ",[17,644,30],{}," or a queue instead.",[41,647,648,654,655,658],{},[625,649,650,653],{},[17,651,652],{},"Condition.wait()"," without a loop."," Spurious wakeups and other waiters mean the predicate must be re-checked. ",[17,656,657],{},"cv.wait_for(predicate, timeout=5)"," does this correctly and should be the default.",[41,660,661,664,665,20,667,20,670,27,673,676,677,66],{},[625,662,663],{},"No timeouts."," Every ",[17,666,19],{},[17,668,669],{},"Event.wait",[17,671,672],{},"acquire",[17,674,675],{},"join"," takes one. Without them a coordination mistake becomes a hung suite instead of a failing test — see ",[62,678,680],{"href":679},"\u002Ftesting-async-and-concurrent-python\u002Ftimeouts-cancellation-and-deadlines\u002Ffailing-fast-with-pytest-timeout\u002F","failing fast with pytest-timeout",[41,682,683,686,687,690,691,694],{},[625,684,685],{},"Threading primitives inside coroutines."," ",[17,688,689],{},"threading.Event().wait()"," blocks the event loop rather than yielding, so a coroutine waiting on one deadlocks everything. Use the ",[17,692,693],{},"asyncio"," equivalents.",[33,696,698],{"id":697},"making-worker-failures-visible","Making worker failures visible",[10,700,701,702,704,705,708],{},"Every pattern above depends on one thing that ",[17,703,46],{}," does not give you: an exception in a worker reaching the test. A raw ",[17,706,707],{},"Thread"," prints the traceback to stderr and then discards it, so the test carries on and usually fails later with a message about the wrong thing.",[78,710,712],{"className":80,"code":711,"language":82,"meta":83,"style":83},"import concurrent.futures\nimport threading\n\n\ndef run_workers(target, count, *, timeout=10):\n    \"\"\"Run `count` copies of `target` concurrently and re-raise any failure.\"\"\"\n    gate = threading.Barrier(count, timeout=timeout)\n\n    def wrapped():\n        gate.wait()\n        return target()\n\n    with concurrent.futures.ThreadPoolExecutor(max_workers=count) as pool:\n        futures = [pool.submit(wrapped) for _ in range(count)]\n        # result() re-raises in the calling thread, so worker failures fail\n        # the test with their own traceback rather than vanishing.\n        return [future.result(timeout=timeout) for future in futures]\n",[17,713,714,719,723,727,731,736,741,746,750,755,760,765,769,774,779,784,789],{"__ignoreMap":83},[87,715,716],{"class":89,"line":90},[87,717,718],{},"import concurrent.futures\n",[87,720,721],{"class":89,"line":96},[87,722,93],{},[87,724,725],{"class":89,"line":103},[87,726,100],{"emptyLinePlaceholder":99},[87,728,729],{"class":89,"line":109},[87,730,100],{"emptyLinePlaceholder":99},[87,732,733],{"class":89,"line":114},[87,734,735],{},"def run_workers(target, count, *, timeout=10):\n",[87,737,738],{"class":89,"line":119},[87,739,740],{},"    \"\"\"Run `count` copies of `target` concurrently and re-raise any failure.\"\"\"\n",[87,742,743],{"class":89,"line":125},[87,744,745],{},"    gate = threading.Barrier(count, timeout=timeout)\n",[87,747,748],{"class":89,"line":131},[87,749,100],{"emptyLinePlaceholder":99},[87,751,752],{"class":89,"line":137},[87,753,754],{},"    def wrapped():\n",[87,756,757],{"class":89,"line":143},[87,758,759],{},"        gate.wait()\n",[87,761,762],{"class":89,"line":149},[87,763,764],{},"        return target()\n",[87,766,767],{"class":89,"line":155},[87,768,100],{"emptyLinePlaceholder":99},[87,770,771],{"class":89,"line":161},[87,772,773],{},"    with concurrent.futures.ThreadPoolExecutor(max_workers=count) as pool:\n",[87,775,776],{"class":89,"line":167},[87,777,778],{},"        futures = [pool.submit(wrapped) for _ in range(count)]\n",[87,780,781],{"class":89,"line":172},[87,782,783],{},"        # result() re-raises in the calling thread, so worker failures fail\n",[87,785,786],{"class":89,"line":178},[87,787,788],{},"        # the test with their own traceback rather than vanishing.\n",[87,790,791],{"class":89,"line":184},[87,792,793],{},"        return [future.result(timeout=timeout) for future in futures]\n",[78,795,797],{"className":80,"code":796,"language":82,"meta":83,"style":83},"def test_counter_is_atomic_under_contention():\n    counter = Counter()\n    run_workers(lambda: [counter.increment() for _ in range(2_000)], count=8)\n    assert counter.value == 16_000\n",[17,798,799,803,807,812],{"__ignoreMap":83},[87,800,801],{"class":89,"line":90},[87,802,128],{},[87,804,805],{"class":89,"line":96},[87,806,134],{},[87,808,809],{"class":89,"line":103},[87,810,811],{},"    run_workers(lambda: [counter.increment() for _ in range(2_000)], count=8)\n",[87,813,814],{"class":89,"line":109},[87,815,816],{},"    assert counter.value == 16_000\n",[10,818,819],{},"The helper folds the barrier, the joining and the exception propagation into four lines of test, which is what makes it realistic to write these tests routinely rather than only when a bug forces the issue.",[10,821,822,823,825,826,829],{},"Two details in the helper are deliberate. The barrier is sized to the worker count and shares the same timeout as the futures, so a worker that never arrives produces a ",[17,824,631],{}," rather than a hang. And the executor is used as a context manager, whose ",[17,827,828],{},"__exit__"," joins every worker before the block ends — which means an assertion failing after the block cannot tear down state a worker is still using. That ordering is the single most common cause of a concurrency test reporting a confusing secondary error instead of the assertion that actually failed.",[413,831,833,889],{"className":832},[416],[418,834,426,839,426,842,426,845,426,849,426,854,426,860,426,865,426,869,426,873,426,877,426,881,426,885],{"viewBox":835,"role":421,"ariaLabelledBy":836,"xmlns":425},"0 0 800 220",[837,838],"ex-t","ex-d",[428,840,841],{"id":837},"Where a worker exception goes, with and without futures",[432,843,844],{"id":838},"With a raw thread, an exception in the worker is printed to standard error and discarded, so the test continues and fails later on an unrelated assertion. With a ThreadPoolExecutor, the exception is stored on the future and re-raised when result is called, so the test fails with the worker's own traceback.",[436,846],{"x":438,"y":438,"width":847,"height":848,"rx":441,"fill":442},"800","220",[444,850,853],{"x":851,"y":447,"textAnchor":448,"fontSize":852,"fontWeight":450,"fill":451},"400","15.5","A silent worker failure is worse than a loud one",[436,855],{"x":856,"y":857,"width":858,"height":468,"rx":474,"fill":859,"stroke":557,"strokeWidth":461},"26","50","748","#fbe9e3",[444,861,864],{"x":862,"y":863,"fontSize":459,"fontWeight":450,"fill":451},"46","74","threading.Thread",[444,866,868],{"x":862,"y":867,"fontSize":474,"fill":451},"96","worker raises → traceback printed to stderr → discarded → test continues",[444,870,872],{"x":862,"y":871,"fontSize":474,"fill":495},"114","the test fails later, on an assertion that was never the problem",[436,874],{"x":856,"y":875,"width":858,"height":468,"rx":474,"fill":876,"stroke":460,"strokeWidth":461},"136","#e6f0ea",[444,878,880],{"x":862,"y":879,"fontSize":459,"fontWeight":450,"fill":451},"160","ThreadPoolExecutor + future.result()",[444,882,884],{"x":862,"y":883,"fontSize":474,"fill":451},"182","worker raises → stored on the future → re-raised in the test thread",[444,886,888],{"x":862,"y":887,"fontSize":474,"fill":483},"200","the failure names the worker's own line",[583,890,891,892,895],{},"Where a raw thread is unavoidable, ",[17,893,894],{},"threading.excepthook"," collects worker exceptions into a list the test can assert is empty.",[33,897,899],{"id":898},"asserting-on-a-concurrency-limit","Asserting on a concurrency limit",[10,901,592,902,904],{},[17,903,26],{}," is the natural tool for checking that a pool, a rate limiter or a worker group never exceeds its configured concurrency — but the assertion is usually better written with a counter than with the semaphore itself.",[78,906,908],{"className":80,"code":907,"language":82,"meta":83,"style":83},"import threading\n\n\ndef test_pool_never_exceeds_its_limit(pool):\n    concurrent = 0\n    peak = 0\n    lock = threading.Lock()\n    start = threading.Barrier(12, timeout=5)\n\n    def task():\n        nonlocal concurrent, peak\n        start.wait()\n        with lock:\n            concurrent += 1\n            peak = max(peak, concurrent)      # sampled inside the lock\n        try:\n            time.sleep(0.01)                  # hold the slot briefly\n        finally:\n            with lock:\n                concurrent -= 1\n\n    threads = [threading.Thread(target=pool.submit_and_wait, args=(task,))\n               for _ in range(12)]\n    for thread in threads:\n        thread.start()\n    for thread in threads:\n        thread.join(timeout=10)\n\n    assert peak \u003C= pool.max_workers, f\"peak concurrency {peak} exceeded the limit\"\n    assert peak == pool.max_workers, \"the pool never reached its configured limit\"\n",[17,909,910,914,918,922,927,932,937,941,946,950,955,960,965,969,974,979,983,988,993,998,1003,1007,1012,1017,1021,1025,1029,1034,1038,1043],{"__ignoreMap":83},[87,911,912],{"class":89,"line":90},[87,913,93],{},[87,915,916],{"class":89,"line":96},[87,917,100],{"emptyLinePlaceholder":99},[87,919,920],{"class":89,"line":103},[87,921,100],{"emptyLinePlaceholder":99},[87,923,924],{"class":89,"line":109},[87,925,926],{},"def test_pool_never_exceeds_its_limit(pool):\n",[87,928,929],{"class":89,"line":114},[87,930,931],{},"    concurrent = 0\n",[87,933,934],{"class":89,"line":119},[87,935,936],{},"    peak = 0\n",[87,938,939],{"class":89,"line":125},[87,940,305],{},[87,942,943],{"class":89,"line":131},[87,944,945],{},"    start = threading.Barrier(12, timeout=5)\n",[87,947,948],{"class":89,"line":137},[87,949,100],{"emptyLinePlaceholder":99},[87,951,952],{"class":89,"line":143},[87,953,954],{},"    def task():\n",[87,956,957],{"class":89,"line":149},[87,958,959],{},"        nonlocal concurrent, peak\n",[87,961,962],{"class":89,"line":155},[87,963,964],{},"        start.wait()\n",[87,966,967],{"class":89,"line":161},[87,968,319],{},[87,970,971],{"class":89,"line":167},[87,972,973],{},"            concurrent += 1\n",[87,975,976],{"class":89,"line":172},[87,977,978],{},"            peak = max(peak, concurrent)      # sampled inside the lock\n",[87,980,981],{"class":89,"line":178},[87,982,181],{},[87,984,985],{"class":89,"line":184},[87,986,987],{},"            time.sleep(0.01)                  # hold the slot briefly\n",[87,989,990],{"class":89,"line":190},[87,991,992],{},"        finally:\n",[87,994,995],{"class":89,"line":196},[87,996,997],{},"            with lock:\n",[87,999,1000],{"class":89,"line":202},[87,1001,1002],{},"                concurrent -= 1\n",[87,1004,1005],{"class":89,"line":208},[87,1006,100],{"emptyLinePlaceholder":99},[87,1008,1009],{"class":89,"line":214},[87,1010,1011],{},"    threads = [threading.Thread(target=pool.submit_and_wait, args=(task,))\n",[87,1013,1014],{"class":89,"line":219},[87,1015,1016],{},"               for _ in range(12)]\n",[87,1018,1019],{"class":89,"line":225},[87,1020,228],{},[87,1022,1023],{"class":89,"line":231},[87,1024,234],{},[87,1026,1027],{"class":89,"line":237},[87,1028,228],{},[87,1030,1031],{"class":89,"line":242},[87,1032,1033],{},"        thread.join(timeout=10)\n",[87,1035,1036],{"class":89,"line":248},[87,1037,100],{"emptyLinePlaceholder":99},[87,1039,1040],{"class":89,"line":254},[87,1041,1042],{},"    assert peak \u003C= pool.max_workers, f\"peak concurrency {peak} exceeded the limit\"\n",[87,1044,1045],{"class":89,"line":259},[87,1046,1047],{},"    assert peak == pool.max_workers, \"the pool never reached its configured limit\"\n",[10,1049,1050],{},"Both assertions matter and they pull in opposite directions. The first says the limit was respected; the second says the test actually exercised it, which guards against a false pass where the work finished so quickly that only one task ever ran at a time. A test that only asserts the upper bound passes trivially against a pool with a limit of one.",[10,1052,1053,1054,1057],{},"The brief ",[17,1055,1056],{},"sleep"," in the task body is the one place a sleep is defensible in a concurrency test: it is not waiting for a condition, it is deliberately occupying a slot so that occupancy is observable. Ten milliseconds against a five-second timeout is a comfortable margin, and the barrier ensures every task starts together so the peak is real.",[33,1059,1061],{"id":1060},"coordinating-with-a-condition","Coordinating with a Condition",[10,1063,1064,1066,1067,1070],{},[17,1065,30],{}," is the primitive for \"wait until the shared state satisfies a predicate\", and ",[17,1068,1069],{},"wait_for"," makes it safe to use without the classic spurious-wakeup bug.",[78,1072,1074],{"className":80,"code":1073,"language":82,"meta":83,"style":83},"import threading\n\n\ndef test_worker_processes_items_in_order(worker):\n    cv = threading.Condition()\n    processed: list[int] = []\n\n    def on_item(item):\n        with cv:\n            processed.append(item)\n            cv.notify_all()               # wake the test, which re-checks\n\n    worker.on_item = on_item\n    worker.submit_all([1, 2, 3])\n\n    with cv:\n        # wait_for loops on the predicate, so a spurious wakeup is harmless\n        # and a partial result does not end the wait early.\n        assert cv.wait_for(lambda: len(processed) == 3, timeout=5), processed\n\n    assert processed == [1, 2, 3]\n",[17,1075,1076,1080,1084,1088,1093,1098,1103,1107,1112,1117,1122,1127,1131,1136,1141,1145,1150,1155,1160,1165,1169],{"__ignoreMap":83},[87,1077,1078],{"class":89,"line":90},[87,1079,93],{},[87,1081,1082],{"class":89,"line":96},[87,1083,100],{"emptyLinePlaceholder":99},[87,1085,1086],{"class":89,"line":103},[87,1087,100],{"emptyLinePlaceholder":99},[87,1089,1090],{"class":89,"line":109},[87,1091,1092],{},"def test_worker_processes_items_in_order(worker):\n",[87,1094,1095],{"class":89,"line":114},[87,1096,1097],{},"    cv = threading.Condition()\n",[87,1099,1100],{"class":89,"line":119},[87,1101,1102],{},"    processed: list[int] = []\n",[87,1104,1105],{"class":89,"line":125},[87,1106,100],{"emptyLinePlaceholder":99},[87,1108,1109],{"class":89,"line":131},[87,1110,1111],{},"    def on_item(item):\n",[87,1113,1114],{"class":89,"line":137},[87,1115,1116],{},"        with cv:\n",[87,1118,1119],{"class":89,"line":143},[87,1120,1121],{},"            processed.append(item)\n",[87,1123,1124],{"class":89,"line":149},[87,1125,1126],{},"            cv.notify_all()               # wake the test, which re-checks\n",[87,1128,1129],{"class":89,"line":155},[87,1130,100],{"emptyLinePlaceholder":99},[87,1132,1133],{"class":89,"line":161},[87,1134,1135],{},"    worker.on_item = on_item\n",[87,1137,1138],{"class":89,"line":167},[87,1139,1140],{},"    worker.submit_all([1, 2, 3])\n",[87,1142,1143],{"class":89,"line":172},[87,1144,100],{"emptyLinePlaceholder":99},[87,1146,1147],{"class":89,"line":178},[87,1148,1149],{},"    with cv:\n",[87,1151,1152],{"class":89,"line":184},[87,1153,1154],{},"        # wait_for loops on the predicate, so a spurious wakeup is harmless\n",[87,1156,1157],{"class":89,"line":190},[87,1158,1159],{},"        # and a partial result does not end the wait early.\n",[87,1161,1162],{"class":89,"line":196},[87,1163,1164],{},"        assert cv.wait_for(lambda: len(processed) == 3, timeout=5), processed\n",[87,1166,1167],{"class":89,"line":202},[87,1168,100],{"emptyLinePlaceholder":99},[87,1170,1171],{"class":89,"line":208},[87,1172,1173],{},"    assert processed == [1, 2, 3]\n",[10,1175,1176,1177,1180,1181,1183,1184,1186,1187,1190],{},"The ",[17,1178,1179],{},"assert"," on ",[17,1182,1069],{},"'s return value is what turns a timeout into a readable failure — ",[17,1185,1069],{}," returns ",[17,1188,1189],{},"False"," rather than raising, so a bare call silently proceeds to the next assertion and fails there with a confusing message about list contents.",[413,1192,1194,1238],{"className":1193},[416],[418,1195,426,1200,426,1203,426,1206,426,1209,426,1212,426,1215,426,1218,426,1222,426,1225,426,1228,426,1231,426,1234],{"viewBox":1196,"role":421,"ariaLabelledBy":1197,"xmlns":425},"0 0 800 234",[1198,1199],"cond-t","cond-d",[428,1201,1202],{"id":1198},"Condition.wait_for compared with a bare wait",[432,1204,1205],{"id":1199},"Two paths. A bare wait returns on any notification, including a spurious one or a partial result, so the test proceeds with incomplete state. wait_for re-evaluates the predicate after every wakeup and only returns true when it holds, returning false on timeout so the failure is attributable.",[436,1207],{"x":438,"y":438,"width":847,"height":1208,"rx":441,"fill":442},"234",[444,1210,1211],{"x":851,"y":447,"textAnchor":448,"fontSize":852,"fontWeight":450,"fill":451},"Why the predicate has to be re-checked",[436,1213],{"x":856,"y":456,"width":858,"height":1214,"rx":474,"fill":859,"stroke":557,"strokeWidth":461},"76",[444,1216,1217],{"x":862,"y":1214,"fontSize":459,"fontWeight":450,"fill":451},"cv.wait(timeout=5)",[444,1219,1221],{"x":862,"y":1220,"fontSize":474,"fill":451},"98","returns on the first notify — which may follow item 1 of 3",[444,1223,1224],{"x":862,"y":467,"fontSize":474,"fill":495},"test proceeds with partial state and fails on the wrong assertion",[436,1226],{"x":856,"y":1227,"width":858,"height":1214,"rx":474,"fill":876,"stroke":460,"strokeWidth":461},"142",[444,1229,657],{"x":862,"y":1230,"fontSize":459,"fontWeight":450,"fill":451},"166",[444,1232,1233],{"x":862,"y":457,"fontSize":474,"fill":451},"re-evaluates after every wakeup; returns True only when the predicate holds",[444,1235,1237],{"x":862,"y":1236,"fontSize":474,"fill":483},"208","returns False on timeout, so the assertion names the real problem",[583,1239,1240,1241,1244,1245,1247],{},"The bare form is correct only inside a ",[17,1242,1243],{},"while not predicate()"," loop, which is precisely what ",[17,1246,1069],{}," is.",[33,1249,1251],{"id":1250},"frequently-asked-questions","Frequently Asked Questions",[10,1253,1254,1257,1258,1260,1261,1263],{},[625,1255,1256],{},"When should I use a Barrier rather than an Event?","\nUse a ",[17,1259,19],{}," when N threads must all arrive before any proceeds, which is the right tool for testing contention between identical operations. Use an ",[17,1262,23],{}," when one thread must signal and others must wait, which suits a producer telling the test it has reached a specific point. Barriers coordinate peers; events coordinate a signaller and its listeners.",[10,1265,1266,1269],{},[625,1267,1268],{},"What does BrokenBarrierError mean in a test?","\nA participant failed to reach the barrier — it raised, it exited early, or it timed out — so the barrier is broken and every other waiter is released with that exception. It almost always means a worker crashed before arriving, and the underlying exception is the one to chase.",[10,1271,1272,1275,1276,20,1278,20,1280,27,1282,1284,1285,1287,1288,1291],{},[625,1273,1274],{},"Do these primitives work for asyncio code too?","\nasyncio has its own ",[17,1277,23],{},[17,1279,26],{},[17,1281,30],{},[17,1283,19],{}," that are not interchangeable with the ",[17,1286,46],{}," ones. Never use a threading primitive inside a coroutine: its ",[17,1289,1290],{},"wait"," blocks the whole loop rather than yielding, which turns a coordination into a deadlock.",[33,1293,1295],{"id":1294},"related","Related",[38,1297,1298,1304,1311,1318],{},[41,1299,1300,1303],{},[62,1301,1302],{"href":64},"Testing Threads & Race Conditions"," — the wider workflow these primitives serve.",[41,1305,1306,1310],{},[62,1307,1309],{"href":1308},"\u002Ftesting-async-and-concurrent-python\u002Ftesting-threads-and-race-conditions\u002Freproducing-a-race-condition-deterministically\u002F","Reproducing a Race Condition Deterministically"," — the seam technique for races between different operations.",[41,1312,1313,1317],{},[62,1314,1316],{"href":1315},"\u002Ftesting-async-and-concurrent-python\u002Ftesting-threads-and-race-conditions\u002Fdumping-stacks-on-deadlock-with-faulthandler\u002F","Dumping Stacks on Deadlock with faulthandler"," — what to do when a coordination mistake hangs anyway.",[41,1319,1320,1323],{},[62,1321,1322],{"href":679},"Failing Fast with pytest-timeout"," — the ceiling that keeps these tests from eating a CI job.",[10,1325,1326,1327],{},"← Back to ",[62,1328,1302],{"href":64},[1330,1331,1332],"style",{},"html .default .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .shiki span {color: var(--shiki-default);background: var(--shiki-default-bg);font-style: var(--shiki-default-font-style);font-weight: var(--shiki-default-font-weight);text-decoration: var(--shiki-default-text-decoration);}html .dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}html.dark .shiki span {color: var(--shiki-dark);background: var(--shiki-dark-bg);font-style: var(--shiki-dark-font-style);font-weight: var(--shiki-dark-font-weight);text-decoration: var(--shiki-dark-text-decoration);}",{"title":83,"searchDepth":96,"depth":96,"links":1334},[1335,1336,1337,1338,1339,1340,1341,1342,1343],{"id":35,"depth":96,"text":36},{"id":72,"depth":96,"text":73},{"id":588,"depth":96,"text":589},{"id":618,"depth":96,"text":619},{"id":697,"depth":96,"text":698},{"id":898,"depth":96,"text":899},{"id":1060,"depth":96,"text":1061},{"id":1250,"depth":96,"text":1251},{"id":1294,"depth":96,"text":1295},"Coordinate test threads precisely with Barrier, Event, Semaphore and Condition so concurrency assertions are deterministic rather than probabilistic.","md",{"slug":1347,"type":1348,"breadcrumb":1349,"datePublished":1350,"dateModified":1350,"faq":1351,"howto":1358},"testing-thread-safety-with-barriers-and-events","article","Barriers & Events","2026-09-18",[1352,1354,1356],{"q":1256,"a":1353},"Use a Barrier when N threads must all arrive before any proceeds, which is the right tool for testing contention between identical operations. Use an Event when one thread must signal and others must wait, which suits a producer telling the test it has reached a specific point. Barriers coordinate peers; events coordinate a signaller and its listeners.",{"q":1268,"a":1355},"A participant failed to reach the barrier — it raised, it exited early, or it timed out — so the barrier is broken and every other waiter is released with that exception. It almost always means a worker crashed before arriving, and the underlying exception is the one to chase.",{"q":1274,"a":1357},"asyncio has its own Event, Semaphore, Condition and Barrier that are not interchangeable with the threading ones. Never use a threading primitive inside a coroutine: its wait blocks the whole loop rather than yielding, which turns a coordination into a deadlock.",{"name":1359,"description":1360,"steps":1361},"How to coordinate test threads deterministically","Pick the primitive that matches the coordination, always pass timeouts, and assert on the invariant afterwards.",[1362,1365,1368,1371,1374],{"name":1363,"text":1364},"Choose the primitive from the coordination shape","Barrier for N peers arriving together, Event for one signal to many waiters, Semaphore for a bounded resource, Condition for a state predicate.",{"name":1366,"text":1367},"Pass a timeout to every wait","Give Barrier, Event.wait, Semaphore.acquire and Thread.join explicit timeouts so a coordination mistake fails rather than hangs.",{"name":1369,"text":1370},"Record worker failures explicitly","Collect exceptions in a list or use futures, since an exception in a raw thread is discarded.",{"name":1372,"text":1373},"Assert on the invariant, not the primitive","Check totals, counts and uniqueness rather than inspecting whether a lock was held.",{"name":1375,"text":1376},"Verify the test fails without the fix","Run it against the unsynchronised implementation and confirm it fails on every attempt.","\u002Ftesting-async-and-concurrent-python\u002Ftesting-threads-and-race-conditions\u002Ftesting-thread-safety-with-barriers-and-events",{"title":5,"description":1344},"testing-async-and-concurrent-python\u002Ftesting-threads-and-race-conditions\u002Ftesting-thread-safety-with-barriers-and-events\u002Findex","g-JdhLT1Bwfn3RRgF9yylIDBHM7h3y_Dnptu4-IXA9c",1789718768452]