Skip to content

Commit 5a1b6fd

Browse files
committed
Incref dict when it's usage is borrowed
1 parent 29c04df commit 5a1b6fd

File tree

2 files changed

+55
-4
lines changed

2 files changed

+55
-4
lines changed

Lib/test/test_free_threading/test_dict.py

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,49 @@ def writer_func(l):
142142
for ref in thread_list:
143143
self.assertIsNone(ref())
144144

145+
def test_racing_set_object_dict(self):
146+
"""Races assigning to __dict__ should be thread safe"""
147+
148+
class C: pass
149+
f = C()
150+
f.__dict__ = {"foo": 42}
151+
THREAD_COUNT = 10
152+
class MyDict(dict): pass
153+
154+
def writer_func():
155+
for i in range(1000):
156+
f.__dict__ = {"foo": 100}
157+
158+
def reader_func():
159+
for i in range(1000):
160+
f.foo
161+
162+
lists = []
163+
readers = []
164+
writers = []
165+
for x in range(THREAD_COUNT):
166+
thread_list = []
167+
lists.append(thread_list)
168+
writer = Thread(target=partial(writer_func))
169+
writers.append(writer)
170+
171+
for x in range(THREAD_COUNT):
172+
thread_list = []
173+
lists.append(thread_list)
174+
reader = Thread(target=partial(reader_func))
175+
readers.append(reader)
176+
177+
for writer in writers:
178+
writer.start()
179+
for reader in readers:
180+
reader.start()
181+
182+
for writer in writers:
183+
writer.join()
184+
185+
for reader in readers:
186+
reader.join()
187+
145188
@unittest.skipIf(_testcapi is None, 'need _testcapi module')
146189
def test_dict_version(self):
147190
dict_version = _testcapi.dict_version

Objects/dictobject.c

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,8 @@ ASSERT_DICT_LOCKED(PyObject *op)
165165

166166
#define IS_DICT_SHARED(mp) _PyObject_GC_IS_SHARED(mp)
167167
#define SET_DICT_SHARED(mp) _PyObject_GC_SET_SHARED(mp)
168+
#define IS_DICT_SHARED_INLINE(mp) _PyObject_GC_IS_SHARED_INLINE(mp)
169+
#define SET_DICT_SHARED_INLINE(mp) _PyObject_GC_SET_SHARED_INLINE(mp)
168170
#define LOAD_INDEX(keys, size, idx) _Py_atomic_load_int##size##_relaxed(&((const int##size##_t*)keys->dk_indices)[idx]);
169171
#define STORE_INDEX(keys, size, idx, value) _Py_atomic_store_int##size##_relaxed(&((int##size##_t*)keys->dk_indices)[idx], (int##size##_t)value);
170172
#define ASSERT_OWNED_OR_SHARED(mp) \
@@ -245,6 +247,8 @@ static inline void split_keys_entry_added(PyDictKeysObject *keys)
245247
#define UNLOCK_KEYS_IF_SPLIT(keys, kind)
246248
#define IS_DICT_SHARED(mp) (false)
247249
#define SET_DICT_SHARED(mp)
250+
#define IS_DICT_SHARED_INLINE(mp) (false)
251+
#define SET_DICT_SHARED_INLINE(mp)
248252
#define LOAD_INDEX(keys, size, idx) ((const int##size##_t*)(keys->dk_indices))[idx]
249253
#define STORE_INDEX(keys, size, idx, value) ((int##size##_t*)(keys->dk_indices))[idx] = (int##size##_t)value
250254

@@ -3082,20 +3086,21 @@ dict_dealloc(PyObject *self)
30823086
/* bpo-31095: UnTrack is needed before calling any callbacks */
30833087
PyObject_GC_UnTrack(mp);
30843088
Py_TRASHCAN_BEGIN(mp, dict_dealloc)
3089+
bool is_shared = IS_DICT_SHARED_INLINE(self);
30853090
if (values != NULL) {
30863091
if (values->embedded == 0) {
30873092
for (i = 0, n = mp->ma_keys->dk_nentries; i < n; i++) {
30883093
Py_XDECREF(values->values[i]);
30893094
}
3090-
free_values(values, false);
3095+
free_values(values, is_shared);
30913096
}
3092-
dictkeys_decref(interp, keys, false);
3097+
dictkeys_decref(interp, keys, is_shared);
30933098
}
30943099
else if (keys != NULL) {
30953100
assert(keys->dk_refcnt == 1 || keys == Py_EMPTY_KEYS);
3096-
dictkeys_decref(interp, keys, false);
3101+
dictkeys_decref(interp, keys, is_shared);
30973102
}
3098-
if (Py_IS_TYPE(mp, &PyDict_Type)) {
3103+
if (Py_IS_TYPE(mp, &PyDict_Type) && !is_shared) {
30993104
_Py_FREELIST_FREE(dicts, mp, Py_TYPE(mp)->tp_free);
31003105
}
31013106
else {
@@ -7047,6 +7052,9 @@ _PyObject_SetManagedDict(PyObject *obj, PyObject *new_dict)
70477052
FT_ATOMIC_STORE_PTR(_PyObject_ManagedDictPointer(obj)->dict,
70487053
(PyDictObject *)Py_XNewRef(new_dict));
70497054
}
7055+
// We could be racing with a read from a borrowed reference of
7056+
// the dict, so we need to free it using QSBR.
7057+
SET_DICT_SHARED_INLINE(dict);
70507058
Py_END_CRITICAL_SECTION2();
70517059

70527060
if (err == 0) {

0 commit comments

Comments
 (0)