| import gc | |
| import time | |
| import unittest | |
| import weakref | |
| from ast import Or | |
| from functools import partial | |
| from threading import Barrier, Thread | |
| from unittest import TestCase | |
| try: | |
| import _testcapi | |
| except ImportError: | |
| _testcapi = None | |
| from test.support import threading_helper | |
| @threading_helper.requires_working_threading() | |
| class TestDict(TestCase): | |
| def test_racing_creation_shared_keys(self): | |
| """Verify that creating dictionaries is thread safe when we | |
| have a type with shared keys""" | |
| class C(int): | |
| pass | |
| self.racing_creation(C) | |
| def test_racing_creation_no_shared_keys(self): | |
| """Verify that creating dictionaries is thread safe when we | |
| have a type with an ordinary dict""" | |
| self.racing_creation(Or) | |
| def test_racing_creation_inline_values_invalid(self): | |
| """Verify that re-creating a dict after we have invalid inline values | |
| is thread safe""" | |
| class C: | |
| pass | |
| def make_obj(): | |
| a = C() | |
| # Make object, make inline values invalid, and then delete dict | |
| a.__dict__ = {} | |
| del a.__dict__ | |
| return a | |
| self.racing_creation(make_obj) | |
| def test_racing_creation_nonmanaged_dict(self): | |
| """Verify that explicit creation of an unmanaged dict is thread safe | |
| outside of the normal attribute setting code path""" | |
| def make_obj(): | |
| def f(): pass | |
| return f | |
| def set(func, name, val): | |
| # Force creation of the dict via PyObject_GenericGetDict | |
| func.__dict__[name] = val | |
| self.racing_creation(make_obj, set) | |
| def racing_creation(self, cls, set=setattr): | |
| objects = [] | |
| processed = [] | |
| OBJECT_COUNT = 100 | |
| THREAD_COUNT = 10 | |
| CUR = 0 | |
| for i in range(OBJECT_COUNT): | |
| objects.append(cls()) | |
| def writer_func(name): | |
| last = -1 | |
| while True: | |
| if CUR == last: | |
| time.sleep(0.001) | |
| continue | |
| elif CUR == OBJECT_COUNT: | |
| break | |
| obj = objects[CUR] | |
| set(obj, name, name) | |
| last = CUR | |
| processed.append(name) | |
| writers = [] | |
| for x in range(THREAD_COUNT): | |
| writer = Thread(target=partial(writer_func, f"a{x:02}")) | |
| writers.append(writer) | |
| writer.start() | |
| for i in range(OBJECT_COUNT): | |
| CUR = i | |
| while len(processed) != THREAD_COUNT: | |
| time.sleep(0.001) | |
| processed.clear() | |
| CUR = OBJECT_COUNT | |
| for writer in writers: | |
| writer.join() | |
| for obj_idx, obj in enumerate(objects): | |
| assert ( | |
| len(obj.__dict__) == THREAD_COUNT | |
| ), f"{len(obj.__dict__)} {obj.__dict__!r} {obj_idx}" | |
| for i in range(THREAD_COUNT): | |
| assert f"a{i:02}" in obj.__dict__, f"a{i:02} missing at {obj_idx}" | |
| def test_racing_set_dict(self): | |
| """Races assigning to __dict__ should be thread safe""" | |
| def f(): pass | |
| l = [] | |
| THREAD_COUNT = 10 | |
| class MyDict(dict): pass | |
| def writer_func(l): | |
| for i in range(1000): | |
| d = MyDict() | |
| l.append(weakref.ref(d)) | |
| f.__dict__ = d | |
| lists = [] | |
| writers = [] | |
| for x in range(THREAD_COUNT): | |
| thread_list = [] | |
| lists.append(thread_list) | |
| writer = Thread(target=partial(writer_func, thread_list)) | |
| writers.append(writer) | |
| for writer in writers: | |
| writer.start() | |
| for writer in writers: | |
| writer.join() | |
| f.__dict__ = {} | |
| gc.collect() | |
| for thread_list in lists: | |
| for ref in thread_list: | |
| self.assertIsNone(ref()) | |
| def test_racing_get_set_dict(self): | |
| """Races getting and setting a dict should be thread safe""" | |
| THREAD_COUNT = 10 | |
| barrier = Barrier(THREAD_COUNT) | |
| def work(d): | |
| barrier.wait() | |
| for _ in range(1000): | |
| d[10] = 0 | |
| d.get(10, None) | |
| _ = d[10] | |
| d = {} | |
| worker_threads = [] | |
| for ii in range(THREAD_COUNT): | |
| worker_threads.append(Thread(target=work, args=[d])) | |
| for t in worker_threads: | |
| t.start() | |
| for t in worker_threads: | |
| t.join() | |
| def test_racing_set_object_dict(self): | |
| """Races assigning to __dict__ should be thread safe""" | |
| class C: pass | |
| class MyDict(dict): pass | |
| for cyclic in (False, True): | |
| f = C() | |
| f.__dict__ = {"foo": 42} | |
| THREAD_COUNT = 10 | |
| def writer_func(l): | |
| for i in range(1000): | |
| if cyclic: | |
| other_d = {} | |
| d = MyDict({"foo": 100}) | |
| if cyclic: | |
| d["x"] = other_d | |
| other_d["bar"] = d | |
| l.append(weakref.ref(d)) | |
| f.__dict__ = d | |
| def reader_func(): | |
| for i in range(1000): | |
| f.foo | |
| lists = [] | |
| readers = [] | |
| writers = [] | |
| for x in range(THREAD_COUNT): | |
| thread_list = [] | |
| lists.append(thread_list) | |
| writer = Thread(target=partial(writer_func, thread_list)) | |
| writers.append(writer) | |
| for x in range(THREAD_COUNT): | |
| reader = Thread(target=partial(reader_func)) | |
| readers.append(reader) | |
| for writer in writers: | |
| writer.start() | |
| for reader in readers: | |
| reader.start() | |
| for writer in writers: | |
| writer.join() | |
| for reader in readers: | |
| reader.join() | |
| f.__dict__ = {} | |
| gc.collect() | |
| gc.collect() | |
| count = 0 | |
| ids = set() | |
| for thread_list in lists: | |
| for i, ref in enumerate(thread_list): | |
| if ref() is None: | |
| continue | |
| count += 1 | |
| ids.add(id(ref())) | |
| count += 1 | |
| self.assertEqual(count, 0) | |
| def test_racing_object_get_set_dict(self): | |
| e = Exception() | |
| def writer(): | |
| for i in range(10000): | |
| e.__dict__ = {1:2} | |
| def reader(): | |
| for i in range(10000): | |
| e.__dict__ | |
| t1 = Thread(target=writer) | |
| t2 = Thread(target=reader) | |
| with threading_helper.start_threads([t1, t2]): | |
| pass | |
| @unittest.skipIf(_testcapi is None, "requires _testcapi") | |
| def test_racing_watch_unwatch_dict(self): | |
| # gh-148393: race between PyDict_Watch / PyDict_Unwatch | |
| # and concurrent dict mutation reading _ma_watcher_tag. | |
| wid = _testcapi.add_dict_watcher(0) | |
| try: | |
| d = {} | |
| ITERS = 1000 | |
| def writer(): | |
| for i in range(ITERS): | |
| d[i] = i | |
| del d[i] | |
| def watcher(): | |
| for _ in range(ITERS): | |
| _testcapi.watch_dict(wid, d) | |
| _testcapi.unwatch_dict(wid, d) | |
| threading_helper.run_concurrently([writer, watcher]) | |
| finally: | |
| _testcapi.clear_dict_watcher(wid) | |
| def test_racing_split_dict_clear_and_lookup(self): | |
| class C: | |
| pass | |
| keys = [f"a{i}" for i in range(16)] | |
| def make_split_nonembedded(): | |
| inst = C() | |
| for key in keys: | |
| setattr(inst, key, keys.index(key)) | |
| # dict.copy() of a split instance dict yields a split table | |
| # with non-embedded values | |
| return inst.__dict__.copy() | |
| d = make_split_nonembedded() | |
| def clearer(): | |
| for _ in range(1000): | |
| d.clear() | |
| d.update(make_split_nonembedded()) | |
| def reader(): | |
| for _ in range(1000): | |
| for k in keys: | |
| d.get(k) | |
| threading_helper.run_concurrently([clearer, reader, reader]) | |
| def test_racing_embedded_values_clear_and_lookup(self): | |
| class C: | |
| pass | |
| obj = C() | |
| def writer(): | |
| for _ in range(1000): | |
| obj.x = 1 | |
| obj.y = 2 | |
| obj.z = 3 | |
| obj.__dict__.clear() | |
| def reader(): | |
| for _ in range(1000): | |
| obj.__dict__.get('x') | |
| threading_helper.run_concurrently([writer, reader, reader]) | |
| def test_racing_dict_update_and_method_lookup(self): | |
| # gh-144295: test race between dict modifications and method lookups. | |
| # Uses BytesIO because the race requires a type without Py_TPFLAGS_INLINE_VALUES | |
| # for the _PyDict_GetMethodStackRef code path. | |
| import io | |
| obj = io.BytesIO() | |
| def writer(): | |
| for _ in range(10000): | |
| obj.x = 1 | |
| del obj.x | |
| def reader(): | |
| for _ in range(10000): | |
| obj.getvalue() | |
| t1 = Thread(target=writer) | |
| t2 = Thread(target=reader) | |
| with threading_helper.start_threads([t1, t2]): | |
| pass | |
| def test_getstate_race_with_shared_keys(self): | |
| box = [None] | |
| enter = Barrier(2) | |
| leave = Barrier(2) | |
| def reader(): | |
| for _ in range(1000): | |
| enter.wait() | |
| box[0].__getstate__() | |
| leave.wait() | |
| def writer(): | |
| for i in range(1000): | |
| box[0] = type(f"C{i}", (), {})() | |
| enter.wait() | |
| setattr(box[0], str(i), 1) | |
| leave.wait() | |
| threading_helper.run_concurrently([reader, writer]) | |
| if __name__ == "__main__": | |
| unittest.main() |