Repository navigation
gh-127716: make memoryview thread-safe in free-threaded build #156248
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
Open
nascheme
wants to merge
18
commits into
python:main
Choose a base branch
from
nascheme:gh-127716-memoryview-thread-safe
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
18 commits
Select commit
Hold shift + click to select a range
8fa2daf
gh-127716: make memoryview thread-safe
weixlu fb7e672
Fix two bugs.
nascheme 908a2a1
Unit test improvements.
nascheme dd6c824
Merge remote-tracking branch 'origin/main' into gh-127716-memoryview-…
nascheme 6692fa5
Address review feedback.
nascheme ab2d602
Merge remote-tracking branch 'origin/main' into gh-127716-memoryview-…
nascheme e3c9012
Remove "while" loop for setting flags.
nascheme 25f396f
Merge remote-tracking branch 'origin/main' into gh-127716-memoryview-…
nascheme 2668057
Handle GC-cleared memoryviews before pinning
nascheme b9b2ad1
Merge remote-tracking branch 'origin/main' into gh-127716-memoryview-…
nascheme a5163e5
Prevent memoryview release during active operations
ngoldbaum e0b6fa5
merge new tests with existing tests
ngoldbaum e8a9aa5
Pin the managed buffer, not the view, during memoryview operations
ngoldbaum b68565c
Don't clear memoryview iterator's reference on exhaustion in free-thr…
ngoldbaum e84805c
Restore memoryview pin helper signatures and release exhausted iterators
ngoldbaum 21b16d4
Merge remote-tracking branch 'origin/main' into gh-127716-memoryview-…
nascheme de2ddbd
Merge branch 'main' into gh-127716-memoryview-thread-safe
kumaraditya303 8833436
Update Objects/memoryobject.c
kumaraditya303 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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,323 @@ | ||
| import pickle | ||
| import threading | ||
| import unittest | ||
|
|
||
| from test.support import threading_helper | ||
|
|
||
| NTHREADS = 4 | ||
| ROUNDS = 4 | ||
| ITERS = 10 | ||
|
|
||
| HASH_DATA_TEMPLATE = bytes(range(256)) | ||
| HASH_DATA_REPEAT = 256 | ||
|
|
||
| READ_OPS = [ | ||
| lambda mv: mv.tobytes(), | ||
| lambda mv: mv.hex(), | ||
| lambda mv: mv.tolist(), | ||
| lambda mv: mv.cast("B"), | ||
| lambda mv: mv.toreadonly(), | ||
| lambda mv: memoryview(mv), | ||
| lambda mv: bytes(mv), | ||
| lambda mv: pickle.PickleBuffer(mv).release(), | ||
| lambda mv: mv.obj, | ||
| lambda mv: mv.format, | ||
| lambda mv: mv.c_contiguous, | ||
| lambda mv: mv[0], | ||
| lambda mv: mv[0:4], | ||
| lambda mv: mv.count(0), | ||
| lambda mv: mv.index(0), | ||
| lambda mv: mv == mv, | ||
| lambda mv: list(mv), | ||
| lambda mv: len(mv), | ||
| ] | ||
|
|
||
|
|
||
| def run_racy(func, *args): | ||
| try: | ||
| func(*args) | ||
| except (ValueError, BufferError): | ||
| pass | ||
|
|
||
|
|
||
| @threading_helper.requires_working_threading() | ||
| class TestMemoryViewRaces(unittest.TestCase): | ||
| def ensure_exporter_free(self, buf): | ||
| # Will raise BufferError if any memoryview still exports it. | ||
| buf.append(0) | ||
| del buf[-1] | ||
|
|
||
| def test_concurrent_slicing_keeps_export_count(self): | ||
| for _ in range(ROUNDS): | ||
| mv = memoryview(bytes(64)) | ||
| slices = [] | ||
| lock = threading.Lock() | ||
|
|
||
| def make_slices(): | ||
| local = [mv[0:4] for _ in range(ITERS)] | ||
| local += [memoryview(mv) for _ in range(ITERS)] | ||
| with lock: | ||
| slices.extend(local) | ||
|
|
||
| threading_helper.run_concurrently(make_slices, nthreads=NTHREADS) | ||
| del slices | ||
|
|
||
| self.assertEqual(bytes(mv[0:4]), b"\x00" * 4) | ||
|
|
||
| def test_concurrent_release(self): | ||
| buf = bytearray(64) | ||
|
|
||
| for _ in range(ROUNDS): | ||
| views = [memoryview(buf) for _ in range(NTHREADS)] | ||
|
|
||
| def release(views=views): | ||
| for mv in views: | ||
| run_racy(mv.release) | ||
|
|
||
| threading_helper.run_concurrently(release, nthreads=NTHREADS) | ||
|
|
||
| self.ensure_exporter_free(buf) | ||
|
|
||
| def test_release_races_with_reads(self): | ||
| for _ in range(ROUNDS): | ||
| buf = bytearray(64) | ||
| cell = [memoryview(buf)] | ||
| lock = threading.Lock() | ||
|
|
||
| def releaser(): | ||
| for _ in range(ITERS): | ||
| mv = cell[0] | ||
| run_racy(mv.release) | ||
| with lock: | ||
| cell[0] = memoryview(buf) | ||
|
|
||
| def reader(): | ||
| for _ in range(ITERS): | ||
| mv = cell[0] | ||
| for op in READ_OPS: | ||
| run_racy(op, mv) | ||
|
|
||
| threading_helper.run_concurrently( | ||
| [releaser] * (NTHREADS // 2) + [reader] * (NTHREADS // 2), | ||
| nthreads=NTHREADS, | ||
| ) | ||
|
|
||
| cell[0].release() | ||
| self.ensure_exporter_free(buf) | ||
|
|
||
| def test_read_keeps_exporter_alive_after_release(self): | ||
| size = 1 << 20 | ||
| for _ in range(ROUNDS): | ||
| exporter = [bytearray(size)] | ||
| view = memoryview(exporter[0]) | ||
| stale = [] | ||
|
|
||
| def reader(): | ||
| try: | ||
| for _ in range(4): | ||
| data = view.tobytes() | ||
| if data.count(0) != len(data): | ||
| stale.append(True) | ||
| return | ||
| except ValueError: | ||
| pass | ||
|
|
||
| def releaser(): | ||
| view.release() | ||
| exporter.clear() | ||
| for _ in range(8): | ||
| bytearray(b"\xdb" * size) | ||
|
|
||
| threading_helper.run_concurrently([reader, releaser]) | ||
| self.assertFalse(stale) | ||
|
|
||
| def test_release_races_with_writes(self): | ||
| for _ in range(ROUNDS): | ||
| buf = bytearray(8 * NTHREADS) | ||
| cell = [memoryview(buf)] | ||
| lock = threading.Lock() | ||
|
|
||
| def releaser(): | ||
| for _ in range(ITERS): | ||
| mv = cell[0] | ||
| run_racy(mv.release) | ||
| with lock: | ||
| cell[0] = memoryview(buf) | ||
|
|
||
| def writer(slot): | ||
| start = slot * 8 | ||
| for _ in range(ITERS): | ||
| mv = cell[0] | ||
| run_racy(mv.__setitem__, start, 1) | ||
| run_racy(mv.__setitem__, slice(start, start + 4), b"abcd") | ||
|
|
||
| workers = [releaser] * (NTHREADS // 2) | ||
| workers += [lambda s=s: writer(s) for s in range(NTHREADS // 2)] | ||
| threading_helper.run_concurrently(workers, nthreads=NTHREADS) | ||
|
|
||
| cell[0].release() | ||
| self.ensure_exporter_free(buf) | ||
|
|
||
| def test_release_races_with_buffer_exports(self): | ||
| for _ in range(ROUNDS): | ||
| buf = bytearray(64) | ||
| mv = memoryview(buf) | ||
|
|
||
| def exporter(): | ||
| for _ in range(ITERS): | ||
| run_racy(lambda: pickle.PickleBuffer(mv).release()) | ||
|
|
||
| def releaser(): | ||
| for _ in range(ITERS): | ||
| run_racy(mv.release) | ||
|
|
||
| threading_helper.run_concurrently( | ||
| [exporter] * (NTHREADS - 1) + [releaser], nthreads=NTHREADS | ||
| ) | ||
|
|
||
| mv.release() | ||
| self.ensure_exporter_free(buf) | ||
|
|
||
| def test_release_with_live_export(self): | ||
| buf = bytearray(64) | ||
|
|
||
| for _ in range(ROUNDS): | ||
| mv = memoryview(buf) | ||
| held = pickle.PickleBuffer(mv) | ||
|
|
||
| def release(mv=mv): | ||
| try: | ||
| mv.release() | ||
| except BufferError: | ||
| pass | ||
|
|
||
| threading_helper.run_concurrently(release, nthreads=NTHREADS) | ||
|
|
||
| self.assertEqual(bytes(mv[0:4]), b"\x00" * 4) | ||
| held.release() | ||
| mv.release() | ||
|
|
||
| self.ensure_exporter_free(buf) | ||
|
|
||
| def test_compare_two_views_races_with_release(self): | ||
| for _ in range(ROUNDS): | ||
| buf = bytearray(64) | ||
| cell = [memoryview(buf), memoryview(buf)] | ||
| lock = threading.Lock() | ||
|
|
||
| def releaser(slot): | ||
| for _ in range(ITERS): | ||
| mv = cell[slot] | ||
| run_racy(mv.release) | ||
| with lock: | ||
| cell[slot] = memoryview(buf) | ||
|
|
||
| def comparer(): | ||
| for _ in range(ITERS): | ||
| left, right = cell[0], cell[1] | ||
| run_racy(lambda: left == right) | ||
| run_racy(lambda: left != right) | ||
|
|
||
| threading_helper.run_concurrently( | ||
| [lambda: releaser(0), lambda: releaser(1)] | ||
| + [comparer] * (NTHREADS - 2), | ||
| nthreads=NTHREADS, | ||
| ) | ||
|
|
||
| for mv in cell: | ||
| mv.release() | ||
| self.ensure_exporter_free(buf) | ||
|
|
||
| def test_release_parent_keeps_child_valid(self): | ||
| for _ in range(ROUNDS): | ||
| buf = bytearray(range(64)) | ||
| parent = memoryview(buf) | ||
| child = parent[0:32] | ||
|
|
||
| def use_child(): | ||
| for _ in range(ITERS): | ||
| self.assertEqual(child[0], 0) | ||
| child.tobytes() | ||
|
|
||
| def release_parent(): | ||
| parent.release() | ||
|
|
||
| threading_helper.run_concurrently( | ||
| [release_parent] + [use_child] * (NTHREADS - 1), | ||
| nthreads=NTHREADS, | ||
| ) | ||
|
|
||
| self.assertEqual(child.tobytes(), bytes(range(32))) | ||
| child.release() | ||
| self.ensure_exporter_free(buf) | ||
|
|
||
| def test_concurrent_iteration(self): | ||
| for _ in range(ROUNDS): | ||
| buf = bytearray(range(64)) | ||
| cell = [memoryview(buf)] | ||
| lock = threading.Lock() | ||
|
|
||
| def releaser(): | ||
| for _ in range(ITERS): | ||
| mv = cell[0] | ||
| run_racy(mv.release) | ||
| with lock: | ||
| cell[0] = memoryview(buf) | ||
|
|
||
| def iterator(): | ||
| for _ in range(ITERS): | ||
| run_racy(list, cell[0]) | ||
|
|
||
| threading_helper.run_concurrently( | ||
| [releaser] * (NTHREADS // 2) + [iterator] * (NTHREADS // 2), | ||
| nthreads=NTHREADS, | ||
| ) | ||
|
|
||
| cell[0].release() | ||
| self.ensure_exporter_free(buf) | ||
|
|
||
| def test_iterator_exhaustion_drops_last_reference(self): | ||
| for _ in range(ROUNDS): | ||
| data = bytearray(range(64)) | ||
| iterator = iter(memoryview(data)) | ||
| values = [] | ||
| lock = threading.Lock() | ||
|
|
||
| def consume(): | ||
| local = list(iterator) | ||
| with lock: | ||
| values.extend(local) | ||
|
|
||
| threading_helper.run_concurrently(consume, nthreads=NTHREADS) | ||
| self.assertEqual(sorted(values), list(data)) | ||
| data.extend(b"x") | ||
| self.assertRaises(StopIteration, next, iterator) | ||
|
|
||
| def test_concurrent_hash(self): | ||
| for _ in range(ROUNDS): | ||
| data = HASH_DATA_TEMPLATE * HASH_DATA_REPEAT | ||
| mv = memoryview(data) | ||
| results = [] | ||
| lock = threading.Lock() | ||
|
|
||
| def hasher(): | ||
| local = {hash(mv) for _ in range(ITERS)} | ||
| with lock: | ||
| results.append(local) | ||
|
|
||
| threading_helper.run_concurrently(hasher, nthreads=NTHREADS) | ||
| self.assertEqual({h for s in results for h in s}, {hash(data)}) | ||
|
|
||
| def test_concurrent_hash_and_release(self): | ||
| for _ in range(ROUNDS): | ||
| mv = memoryview(HASH_DATA_TEMPLATE * HASH_DATA_REPEAT) | ||
|
|
||
| def work(mv=mv): | ||
| run_racy(hash, mv) | ||
| run_racy(mv.release) | ||
|
|
||
| threading_helper.run_concurrently(work, nthreads=NTHREADS) | ||
|
|
||
|
|
||
| if __name__ == "__main__": | ||
| unittest.main() |
Oops, something went wrong.
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.