-
-
Notifications
You must be signed in to change notification settings - Fork 32.2k
gh-115999: Make list and tuple iteration more thread-safe. #128637
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
17 commits
Select commit
Hold shift + click to select a range
1533d1d
Make list, tuple and range iteration more thread-safe, and actually test
Yhg1s a069f9b
Fix lint issues.
Yhg1s 661dc7b
Restore the original guard for list_get_item_ref's fast path, since it's
Yhg1s 7263d1e
Merge branch 'main' into list-realloc
Yhg1s 484c6b2
Fix test failures in test_sys because of the changed size of range
Yhg1s 89c4629
Merge branch 'list-realloc' of github.com:Yhg1s/cpython into list-rea…
Yhg1s daacf82
Fix tupleiter_reduce for test_iter's assumptions, and make tupleiter_…
Yhg1s 94ab0c6
Undo the range iterator changes (leave them for later), adjust the range
Yhg1s c6b9200
Remove edit snafu.
Yhg1s ed7e8c7
Merge branch 'main' into list-realloc
Yhg1s dd56d89
Extend tests a little, add suppressions with links to GH issues.
Yhg1s 4028dfe
Address reviewer comments.
Yhg1s ef5ca7c
Turn the last few stores/loads of list iterator/reversed iterator into
Yhg1s e1dd5f8
Merge branch 'main' into list-realloc
Yhg1s f689a8a
Address reviewer comments.
Yhg1s 2923f6a
Merge branch 'list-realloc' of github.com:Yhg1s/cpython into list-rea…
Yhg1s 08f0647
Merge branch 'main' into list-realloc
Yhg1s File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import threading | ||
import unittest | ||
from test import support | ||
|
||
# The race conditions these tests were written for only happen every now and | ||
# then, even with the current numbers. To find rare race conditions, bumping | ||
# these up will help, but it makes the test runtime highly variable under | ||
# free-threading. Overhead is much higher under ThreadSanitizer, but it's | ||
# also much better at detecting certain races, so we don't need as many | ||
# items/threads. | ||
if support.check_sanitizer(thread=True): | ||
NUMITEMS = 1000 | ||
NUMTHREADS = 2 | ||
else: | ||
NUMITEMS = 100000 | ||
NUMTHREADS = 5 | ||
NUMMUTATORS = 2 | ||
|
||
class ContendedTupleIterationTest(unittest.TestCase): | ||
def make_testdata(self, n): | ||
return tuple(range(n)) | ||
|
||
def assert_iterator_results(self, results, expected): | ||
# Most iterators are not atomic (yet?) so they can skip or duplicate | ||
# items, but they should not invent new items (like the range | ||
# iterator currently does). | ||
extra_items = set(results) - set(expected) | ||
self.assertEqual(set(), extra_items) | ||
|
||
def run_threads(self, func, *args, numthreads=NUMTHREADS): | ||
threads = [] | ||
for _ in range(numthreads): | ||
t = threading.Thread(target=func, args=args) | ||
t.start() | ||
threads.append(t) | ||
return threads | ||
|
||
def test_iteration(self): | ||
"""Test iteration over a shared container""" | ||
seq = self.make_testdata(NUMITEMS) | ||
results = [] | ||
start = threading.Barrier(NUMTHREADS) | ||
def worker(): | ||
idx = 0 | ||
start.wait() | ||
for item in seq: | ||
idx += 1 | ||
results.append(idx) | ||
threads = self.run_threads(worker) | ||
for t in threads: | ||
t.join() | ||
# Each thread has its own iterator, so results should be entirely predictable. | ||
self.assertEqual(results, [NUMITEMS] * NUMTHREADS) | ||
|
||
def test_shared_iterator(self): | ||
"""Test iteration over a shared iterator""" | ||
seq = self.make_testdata(NUMITEMS) | ||
it = iter(seq) | ||
results = [] | ||
start = threading.Barrier(NUMTHREADS) | ||
def worker(): | ||
items = [] | ||
start.wait() | ||
# We want a tight loop, so put items in the shared list at the end. | ||
for item in it: | ||
items.append(item) | ||
results.extend(items) | ||
threads = self.run_threads(worker) | ||
for t in threads: | ||
t.join() | ||
self.assert_iterator_results(results, seq) | ||
|
||
class ContendedListIterationTest(ContendedTupleIterationTest): | ||
def make_testdata(self, n): | ||
return list(range(n)) | ||
|
||
def test_iteration_while_mutating(self): | ||
"""Test iteration over a shared mutating container.""" | ||
seq = self.make_testdata(NUMITEMS) | ||
results = [] | ||
start = threading.Barrier(NUMTHREADS + NUMMUTATORS) | ||
endmutate = threading.Event() | ||
def mutator(): | ||
orig = seq[:] | ||
# Make changes big enough to cause resizing of the list, with | ||
# items shifted around for good measure. | ||
replacement = (orig * 3)[NUMITEMS//2:] | ||
start.wait() | ||
while not endmutate.is_set(): | ||
seq.extend(replacement) | ||
seq[:0] = orig | ||
seq.__imul__(2) | ||
seq.extend(seq) | ||
seq[:] = orig | ||
def worker(): | ||
items = [] | ||
start.wait() | ||
# We want a tight loop, so put items in the shared list at the end. | ||
for item in seq: | ||
items.append(item) | ||
results.extend(items) | ||
mutators = () | ||
try: | ||
threads = self.run_threads(worker) | ||
mutators = self.run_threads(mutator, numthreads=NUMMUTATORS) | ||
for t in threads: | ||
t.join() | ||
finally: | ||
endmutate.set() | ||
for m in mutators: | ||
m.join() | ||
self.assert_iterator_results(results, list(seq)) | ||
|
||
|
||
class ContendedRangeIterationTest(ContendedTupleIterationTest): | ||
def make_testdata(self, n): | ||
return range(n) | ||
|
||
def assert_iterator_results(self, results, expected): | ||
# Range iterators that are shared between threads will (right now) | ||
# sometimes produce items after the end of the range, sometimes | ||
# _far_ after the end of the range. That should be fixed, but for | ||
# now, let's just check they're integers that could have resulted | ||
# from stepping beyond the range bounds. | ||
extra_items = set(results) - set(expected) | ||
for item in extra_items: | ||
self.assertEqual((item - expected.start) % expected.step, 0) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.