cpython/Lib/test/test_free_threading/test_races.py
Neil Schemenauer 1b15c89a17
gh-115999: Specialize STORE_ATTR in free-threaded builds. (gh-127838)
* Add `_PyDictKeys_StringLookupSplit` which does locking on dict keys and
  use in place of `_PyDictKeys_StringLookup`.

* Change `_PyObject_TryGetInstanceAttribute` to use that function
  in the case of split keys.

* Add `unicodekeys_lookup_split` helper which allows code sharing
  between `_Py_dict_lookup` and `_PyDictKeys_StringLookupSplit`.

* Fix locking for `STORE_ATTR_INSTANCE_VALUE`.  Create
  `_GUARD_TYPE_VERSION_AND_LOCK` uop so that object stays locked and
  `tp_version_tag` cannot change.

* Pass `tp_version_tag` to `specialize_dict_access()`, ensuring
  the version we store on the cache is the correct one (in case of
  it changing during the specalize analysis).

* Split `analyze_descriptor` into `analyze_descriptor_load` and
  `analyze_descriptor_store` since those don't share much logic.
  Add `descriptor_is_class` helper function.

* In `specialize_dict_access`, double check `_PyObject_GetManagedDict()`
  in case we race and dict was materialized before the lock.

* Avoid borrowed references in `_Py_Specialize_StoreAttr()`.

* Use `specialize()` and `unspecialize()` helpers.

* Add unit tests to ensure specializing happens as expected in FT builds.

* Add unit tests to attempt to trigger data races (useful for running under TSAN).

* Add `has_split_table` function to `_testinternalcapi`.
2024-12-19 10:21:17 -08:00

275 lines
6.6 KiB
Python

# It's most useful to run these tests with ThreadSanitizer enabled.
import sys
import functools
import threading
import time
import unittest
import _testinternalcapi
from test.support import threading_helper
class TestBase(unittest.TestCase):
pass
def do_race(func1, func2):
"""Run func1() and func2() repeatedly in separate threads."""
n = 1000
barrier = threading.Barrier(2)
def repeat(func):
barrier.wait()
for _i in range(n):
func()
threads = [
threading.Thread(target=functools.partial(repeat, func1)),
threading.Thread(target=functools.partial(repeat, func2)),
]
for thread in threads:
thread.start()
for thread in threads:
thread.join()
@threading_helper.requires_working_threading()
class TestRaces(TestBase):
def test_racing_cell_set(self):
"""Test cell object gettr/settr properties."""
def nested_func():
x = 0
def inner():
nonlocal x
x += 1
# This doesn't race because LOAD_DEREF and STORE_DEREF on the
# cell object use critical sections.
do_race(nested_func, nested_func)
def nested_func2():
x = 0
def inner():
y = x
frame = sys._getframe(1)
frame.f_locals["x"] = 2
return inner
def mutate_func2():
inner = nested_func2()
cell = inner.__closure__[0]
old_value = cell.cell_contents
cell.cell_contents = 1000
time.sleep(0)
cell.cell_contents = old_value
time.sleep(0)
# This revealed a race with cell_set_contents() since it was missing
# the critical section.
do_race(nested_func2, mutate_func2)
def test_racing_cell_cmp_repr(self):
"""Test cell object compare and repr methods."""
def nested_func():
x = 0
y = 0
def inner():
return x + y
return inner.__closure__
cell_a, cell_b = nested_func()
def mutate():
cell_a.cell_contents += 1
def access():
cell_a == cell_b
s = repr(cell_a)
# cell_richcompare() and cell_repr used to have data races
do_race(mutate, access)
def test_racing_load_super_attr(self):
"""Test (un)specialization of LOAD_SUPER_ATTR opcode."""
class C:
def __init__(self):
try:
super().__init__
super().__init__()
except RuntimeError:
pass # happens if __class__ is replaced with non-type
def access():
C()
def mutate():
# Swap out the super() global with a different one
real_super = super
globals()["super"] = lambda s=1: s
time.sleep(0)
globals()["super"] = real_super
time.sleep(0)
# Swap out the __class__ closure value with a non-type
cell = C.__init__.__closure__[0]
real_class = cell.cell_contents
cell.cell_contents = 99
time.sleep(0)
cell.cell_contents = real_class
# The initial PR adding specialized opcodes for LOAD_SUPER_ATTR
# had some races (one with the super() global changing and one
# with the cell binding being changed).
do_race(access, mutate)
def test_racing_to_bool(self):
seq = [1]
class C:
def __bool__(self):
return False
def access():
if seq:
return 1
else:
return 2
def mutate():
nonlocal seq
seq = [1]
time.sleep(0)
seq = C()
time.sleep(0)
do_race(access, mutate)
def test_racing_store_attr_slot(self):
class C:
__slots__ = ['x', '__dict__']
c = C()
def set_slot():
for i in range(10):
c.x = i
time.sleep(0)
def change_type():
def set_x(self, x):
pass
def get_x(self):
pass
C.x = property(get_x, set_x)
time.sleep(0)
del C.x
time.sleep(0)
do_race(set_slot, change_type)
def set_getattribute():
C.__getattribute__ = lambda self, x: x
time.sleep(0)
del C.__getattribute__
time.sleep(0)
do_race(set_slot, set_getattribute)
def test_racing_store_attr_instance_value(self):
class C:
pass
c = C()
def set_value():
for i in range(100):
c.x = i
set_value()
def read():
x = c.x
def mutate():
# Adding a property for 'x' should unspecialize it.
C.x = property(lambda self: None, lambda self, x: None)
time.sleep(0)
del C.x
time.sleep(0)
do_race(read, mutate)
def test_racing_store_attr_with_hint(self):
class C:
pass
c = C()
for i in range(29):
setattr(c, f"_{i}", None)
def set_value():
for i in range(100):
c.x = i
set_value()
def read():
x = c.x
def mutate():
# Adding a property for 'x' should unspecialize it.
C.x = property(lambda self: None, lambda self, x: None)
time.sleep(0)
del C.x
time.sleep(0)
do_race(read, mutate)
def make_shared_key_dict(self):
class C:
pass
a = C()
a.x = 1
return a.__dict__
def test_racing_store_attr_dict(self):
"""Test STORE_ATTR with various dictionary types."""
class C:
pass
c = C()
def set_value():
for i in range(20):
c.x = i
def mutate():
nonlocal c
c.x = 1
self.assertTrue(_testinternalcapi.has_inline_values(c))
for i in range(30):
setattr(c, f"_{i}", None)
self.assertFalse(_testinternalcapi.has_inline_values(c.__dict__))
c.__dict__ = self.make_shared_key_dict()
self.assertTrue(_testinternalcapi.has_split_table(c.__dict__))
c.__dict__[1] = None
self.assertFalse(_testinternalcapi.has_split_table(c.__dict__))
c = C()
do_race(set_value, mutate)
if __name__ == "__main__":
unittest.main()