Repository navigation
set.intersection_update() can lose concurrent updates in free-threaded builds #158600
Description
Activity
- addedtype-bugAn unexpected behavior, bug, or errorAn unexpected behavior, bug, or error
on Oct 2, 2026 - addedinterpreter-core(Objects, Python, Grammar, and Parser dirs)(Objects, Python, Grammar, and Parser dirs)
on Oct 2, 2026 I believe
intersection_update()must be the same asx = x & other(atomically), or at least produce the same result as the GIL build. How does__iand__work? is it the same code but everything is in a critical section?hosu3das20s05s commented
on Oct 2, 2026 on Oct 2, 2026 via email · Hidden as spamshow commentMore actionsI think this is a bug. The cause is that
set.__iand__uses a larger critical section. Both__iand__andintersection_update()first compute the expected result into a temporary settmp, and then swaptmpinto the target set. The difference is that__iand__computestmpwhile holding the critical section, which prevents two intersections on the same set from running concurrently. In other words, they are forced to execute one after another instead of overlapping. I will open a PR to fix this.I would be interested in preparing a fix if this is confirmed to be a bug.
@hetaozdh I'm currently working on a PR for this issue. I would appreciate it if you could leave this one to me for now. Thank you!
Reacted by he_taoYes. Thank you for what you did!
How does
__iand__work? is it the same code but everything is in a critical section?For how
__iand__is implemented, yes, both the calculation and assignment are protected byPy_BEGIN_CRITICAL_SECTION(...), butintersection_update()has a critical section gap between the calculation and assignment, and this is a place where a race condition can occur. However, becauseintersection_update()accepts several operands, while__iand__accepts only one operand, their implementations are not the same. But this leads to a situation where both do not give the same result, either with or without the GIL.I propose a way to fix this problem by adding a critical section that covers both calculation and assignment. I will open a PR to show how I implement this. I will include more details in the PR's comment, but if you have any feedback, we can discuss it further!
- added 8 commits that reference this issue
on Oct 4, 2026
Bug report
Summary
In a free-threaded build, concurrent calls to
set.intersection_update()on the same set can produce a non-deterministic result, which I did not see it with-X gil=1. One successful intersection can be lost when another thread later computes and assigns a result calculated from an older target state. The result fromset.intersection_update()differs froms &= other(s.__iand__(other)), although both have the same single-threaded result.Reproducer
AI Disclosure: This code is generated by Codex, but I have already verified it.
One observed result with
-X gil=0:A result with
-X gil=1:Expected behavior
For sets
AthroughD, every serial order must produce:In this reproducer, it is
set(range(N)), with 20,000 items.Why the result cannot be serialized
Let me simplify this example to have only two sets. When there are an initial set and two operand sets, the valid serial executions are as follows, where both include both intersections.
However, how the current
set.intersection_update()works can be visualized (in a simple way) as follows.The last replacement wins. Therefore, it can leave only
original_target & B, ororiginal_target & Adepending on an execution order, which is not equivalent to either serial ordering.A Possible Cause
set.intersection_update()causesset_intersection_update_multi_impl()inObjects/setobject.c. This method first calculates a temporary result:Then, it separately takes the target's critical section and swaps the whole target body:
Another
intersection_update()in another thread can calculate its temporary result after the first calculation has finished, but before the first swap occurs. Its later swap can overwrite the first update.On the contrary,
__iand__reachesset_iand(), which holds a critical section across both calculation and assignment.Question
Is this behavior expected? Is
set.intersection_update()intended to be linearizable with respect to concurrent updates of the same target set, given that its operands are sets that are not modified concurrently?I would be interested in preparing a fix if this is confirmed to be a bug.
CPython versions tested on:
CPython main branch
Operating systems tested on:
Linux, Windows
Linked PRs