Skip to content

Commit f939b3c

Browse files
committed
Issue #28427: old keys should not remove new values from
WeakValueDictionary when collecting from another thread.
1 parent 994f04d commit f939b3c

File tree

6 files changed

+147
-15
lines changed

6 files changed

+147
-15
lines changed

Include/dictobject.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,9 @@ PyAPI_FUNC(PyObject *) PyDict_GetItem(PyObject *mp, PyObject *key);
111111
PyAPI_FUNC(PyObject *) _PyDict_GetItemWithError(PyObject *mp, PyObject *key);
112112
PyAPI_FUNC(int) PyDict_SetItem(PyObject *mp, PyObject *key, PyObject *item);
113113
PyAPI_FUNC(int) PyDict_DelItem(PyObject *mp, PyObject *key);
114+
PyAPI_FUNC(int) _PyDict_DelItemIf(PyObject *mp, PyObject *key,
115+
int (*predicate)(PyObject *value));
116+
114117
PyAPI_FUNC(void) PyDict_Clear(PyObject *mp);
115118
PyAPI_FUNC(int) PyDict_Next(
116119
PyObject *mp, Py_ssize_t *pos, PyObject **key, PyObject **value);

Lib/test/test_weakref.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1437,6 +1437,18 @@ def test_threaded_weak_valued_pop(self):
14371437
x = d.pop(10, 10)
14381438
self.assertIsNot(x, None) # we never put None in there!
14391439

1440+
def test_threaded_weak_valued_consistency(self):
1441+
# Issue #28427: old keys should not remove new values from
1442+
# WeakValueDictionary when collecting from another thread.
1443+
d = weakref.WeakValueDictionary()
1444+
with collect_in_thread():
1445+
for i in range(200000):
1446+
o = RefCycle()
1447+
d[10] = o
1448+
# o is still alive, so the dict can't be empty
1449+
self.assertEqual(len(d), 1)
1450+
o = None # lose ref
1451+
14401452

14411453
from test import mapping_tests
14421454

Lib/weakref.py

Lines changed: 35 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@
1818
proxy,
1919
CallableProxyType,
2020
ProxyType,
21-
ReferenceType)
21+
ReferenceType,
22+
_remove_dead_weakref)
2223

2324
from _weakrefset import WeakSet, _IterationGuard
2425

@@ -58,7 +59,9 @@ def remove(wr, selfref=ref(self)):
5859
if self._iterating:
5960
self._pending_removals.append(wr.key)
6061
else:
61-
del self.data[wr.key]
62+
# Atomic removal is necessary since this function
63+
# can be called asynchronously by the GC
64+
_remove_dead_weakref(self.data, wr.key)
6265
self._remove = remove
6366
# A list of keys to be removed
6467
self._pending_removals = []
@@ -71,9 +74,12 @@ def _commit_removals(self):
7174
# We shouldn't encounter any KeyError, because this method should
7275
# always be called *before* mutating the dict.
7376
while l:
74-
del d[l.pop()]
77+
key = l.pop()
78+
_remove_dead_weakref(d, key)
7579

7680
def __getitem__(self, key):
81+
if self._pending_removals:
82+
self._commit_removals()
7783
o = self.data[key]()
7884
if o is None:
7985
raise KeyError, key
@@ -86,13 +92,17 @@ def __delitem__(self, key):
8692
del self.data[key]
8793

8894
def __contains__(self, key):
95+
if self._pending_removals:
96+
self._commit_removals()
8997
try:
9098
o = self.data[key]()
9199
except KeyError:
92100
return False
93101
return o is not None
94102

95103
def has_key(self, key):
104+
if self._pending_removals:
105+
self._commit_removals()
96106
try:
97107
o = self.data[key]()
98108
except KeyError:
@@ -113,6 +123,8 @@ def clear(self):
113123
self.data.clear()
114124

115125
def copy(self):
126+
if self._pending_removals:
127+
self._commit_removals()
116128
new = WeakValueDictionary()
117129
for key, wr in self.data.items():
118130
o = wr()
@@ -124,6 +136,8 @@ def copy(self):
124136

125137
def __deepcopy__(self, memo):
126138
from copy import deepcopy
139+
if self._pending_removals:
140+
self._commit_removals()
127141
new = self.__class__()
128142
for key, wr in self.data.items():
129143
o = wr()
@@ -132,6 +146,8 @@ def __deepcopy__(self, memo):
132146
return new
133147

134148
def get(self, key, default=None):
149+
if self._pending_removals:
150+
self._commit_removals()
135151
try:
136152
wr = self.data[key]
137153
except KeyError:
@@ -145,6 +161,8 @@ def get(self, key, default=None):
145161
return o
146162

147163
def items(self):
164+
if self._pending_removals:
165+
self._commit_removals()
148166
L = []
149167
for key, wr in self.data.items():
150168
o = wr()
@@ -153,13 +171,17 @@ def items(self):
153171
return L
154172

155173
def iteritems(self):
174+
if self._pending_removals:
175+
self._commit_removals()
156176
with _IterationGuard(self):
157177
for wr in self.data.itervalues():
158178
value = wr()
159179
if value is not None:
160180
yield wr.key, value
161181

162182
def iterkeys(self):
183+
if self._pending_removals:
184+
self._commit_removals()
163185
with _IterationGuard(self):
164186
for k in self.data.iterkeys():
165187
yield k
@@ -176,11 +198,15 @@ def itervaluerefs(self):
176198
keep the values around longer than needed.
177199
178200
"""
201+
if self._pending_removals:
202+
self._commit_removals()
179203
with _IterationGuard(self):
180204
for wr in self.data.itervalues():
181205
yield wr
182206

183207
def itervalues(self):
208+
if self._pending_removals:
209+
self._commit_removals()
184210
with _IterationGuard(self):
185211
for wr in self.data.itervalues():
186212
obj = wr()
@@ -212,13 +238,13 @@ def pop(self, key, *args):
212238
return o
213239

214240
def setdefault(self, key, default=None):
241+
if self._pending_removals:
242+
self._commit_removals()
215243
try:
216244
o = self.data[key]()
217245
except KeyError:
218246
o = None
219247
if o is None:
220-
if self._pending_removals:
221-
self._commit_removals()
222248
self.data[key] = KeyedRef(default, self._remove, key)
223249
return default
224250
else:
@@ -254,9 +280,13 @@ def valuerefs(self):
254280
keep the values around longer than needed.
255281
256282
"""
283+
if self._pending_removals:
284+
self._commit_removals()
257285
return self.data.values()
258286

259287
def values(self):
288+
if self._pending_removals:
289+
self._commit_removals()
260290
L = []
261291
for wr in self.data.values():
262292
o = wr()

Misc/NEWS

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ Core and Builtins
1515
Library
1616
-------
1717

18+
- Issue #28427: old keys should not remove new values from
19+
WeakValueDictionary when collecting from another thread.
20+
1821
- Issue #28998: More APIs now support longs as well as ints.
1922

2023
- Issue 28923: Remove editor artifacts from Tix.py,

Modules/_weakref.c

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,43 @@
55
((PyWeakReference **) PyObject_GET_WEAKREFS_LISTPTR(o))
66

77

8+
static int
9+
is_dead_weakref(PyObject *value)
10+
{
11+
if (!PyWeakref_Check(value)) {
12+
PyErr_SetString(PyExc_TypeError, "not a weakref");
13+
return -1;
14+
}
15+
return PyWeakref_GET_OBJECT(value) == Py_None;
16+
}
17+
18+
PyDoc_STRVAR(remove_dead_weakref__doc__,
19+
"_remove_dead_weakref(dict, key) -- atomically remove key from dict\n"
20+
"if it points to a dead weakref.");
21+
22+
static PyObject *
23+
remove_dead_weakref(PyObject *self, PyObject *args)
24+
{
25+
PyObject *dct, *key;
26+
27+
if (!PyArg_ParseTuple(args, "O!O:_remove_dead_weakref",
28+
&PyDict_Type, &dct, &key)) {
29+
return NULL;
30+
}
31+
if (_PyDict_DelItemIf(dct, key, is_dead_weakref) < 0) {
32+
if (PyErr_ExceptionMatches(PyExc_KeyError))
33+
/* This function is meant to allow safe weak-value dicts
34+
with GC in another thread (see issue #28427), so it's
35+
ok if the key doesn't exist anymore.
36+
*/
37+
PyErr_Clear();
38+
else
39+
return NULL;
40+
}
41+
Py_RETURN_NONE;
42+
}
43+
44+
845
PyDoc_STRVAR(weakref_getweakrefcount__doc__,
946
"getweakrefcount(object) -- return the number of weak references\n"
1047
"to 'object'.");
@@ -84,6 +121,8 @@ weakref_functions[] = {
84121
weakref_getweakrefs__doc__},
85122
{"proxy", weakref_proxy, METH_VARARGS,
86123
weakref_proxy__doc__},
124+
{"_remove_dead_weakref", remove_dead_weakref, METH_VARARGS,
125+
remove_dead_weakref__doc__},
87126
{NULL, NULL, 0, NULL}
88127
};
89128

Objects/dictobject.c

Lines changed: 55 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -848,13 +848,28 @@ PyDict_SetItem(register PyObject *op, PyObject *key, PyObject *value)
848848
return dict_set_item_by_hash_or_entry(op, key, hash, NULL, value);
849849
}
850850

851+
static int
852+
delitem_common(PyDictObject *mp, PyDictEntry *ep)
853+
{
854+
PyObject *old_value, *old_key;
855+
856+
old_key = ep->me_key;
857+
Py_INCREF(dummy);
858+
ep->me_key = dummy;
859+
old_value = ep->me_value;
860+
ep->me_value = NULL;
861+
mp->ma_used--;
862+
Py_DECREF(old_value);
863+
Py_DECREF(old_key);
864+
return 0;
865+
}
866+
851867
int
852868
PyDict_DelItem(PyObject *op, PyObject *key)
853869
{
854870
register PyDictObject *mp;
855871
register long hash;
856872
register PyDictEntry *ep;
857-
PyObject *old_value, *old_key;
858873

859874
if (!PyDict_Check(op)) {
860875
PyErr_BadInternalCall();
@@ -875,15 +890,45 @@ PyDict_DelItem(PyObject *op, PyObject *key)
875890
set_key_error(key);
876891
return -1;
877892
}
878-
old_key = ep->me_key;
879-
Py_INCREF(dummy);
880-
ep->me_key = dummy;
881-
old_value = ep->me_value;
882-
ep->me_value = NULL;
883-
mp->ma_used--;
884-
Py_DECREF(old_value);
885-
Py_DECREF(old_key);
886-
return 0;
893+
894+
return delitem_common(mp, ep);
895+
}
896+
897+
int
898+
_PyDict_DelItemIf(PyObject *op, PyObject *key,
899+
int (*predicate)(PyObject *value))
900+
{
901+
register PyDictObject *mp;
902+
register long hash;
903+
register PyDictEntry *ep;
904+
int res;
905+
906+
if (!PyDict_Check(op)) {
907+
PyErr_BadInternalCall();
908+
return -1;
909+
}
910+
assert(key);
911+
if (!PyString_CheckExact(key) ||
912+
(hash = ((PyStringObject *) key)->ob_shash) == -1) {
913+
hash = PyObject_Hash(key);
914+
if (hash == -1)
915+
return -1;
916+
}
917+
mp = (PyDictObject *)op;
918+
ep = (mp->ma_lookup)(mp, key, hash);
919+
if (ep == NULL)
920+
return -1;
921+
if (ep->me_value == NULL) {
922+
set_key_error(key);
923+
return -1;
924+
}
925+
res = predicate(ep->me_value);
926+
if (res == -1)
927+
return -1;
928+
if (res > 0)
929+
return delitem_common(mp, ep);
930+
else
931+
return 0;
887932
}
888933

889934
void

0 commit comments

Comments
 (0)