diff options
author | Antoine Pitrou <solipsis@pitrou.net> | 2016-12-27 13:34:54 (GMT) |
---|---|---|
committer | Antoine Pitrou <solipsis@pitrou.net> | 2016-12-27 13:34:54 (GMT) |
commit | c06ae208ebd55ff261438385c4179023f2df0388 (patch) | |
tree | fbf18b5e4e539f2fc4ca50c36023b43b6f62ee41 | |
parent | a171a03bd23c0fb9b424cce879d4c0b53fc331f6 (diff) | |
parent | d741ed492f17609109432f1bccac0c019a05471b (diff) | |
download | cpython-c06ae208ebd55ff261438385c4179023f2df0388.zip cpython-c06ae208ebd55ff261438385c4179023f2df0388.tar.gz cpython-c06ae208ebd55ff261438385c4179023f2df0388.tar.bz2 |
Issue #28427: old keys should not remove new values from
WeakValueDictionary when collecting from another thread.
-rw-r--r-- | Include/dictobject.h | 2 | ||||
-rw-r--r-- | Lib/test/test_weakref.py | 12 | ||||
-rw-r--r-- | Lib/weakref.py | 34 | ||||
-rw-r--r-- | Misc/NEWS | 3 | ||||
-rw-r--r-- | Modules/_weakref.c | 41 | ||||
-rw-r--r-- | Modules/clinic/_weakref.c.h | 32 | ||||
-rw-r--r-- | Objects/dictobject.c | 90 |
7 files changed, 193 insertions, 21 deletions
diff --git a/Include/dictobject.h b/Include/dictobject.h index 885694b..fd423a3 100644 --- a/Include/dictobject.h +++ b/Include/dictobject.h @@ -88,6 +88,8 @@ PyAPI_FUNC(int) PyDict_DelItem(PyObject *mp, PyObject *key); #ifndef Py_LIMITED_API PyAPI_FUNC(int) _PyDict_DelItem_KnownHash(PyObject *mp, PyObject *key, Py_hash_t hash); +PyAPI_FUNC(int) _PyDict_DelItemIf(PyObject *mp, PyObject *key, + int (*predicate)(PyObject *value)); #endif PyAPI_FUNC(void) PyDict_Clear(PyObject *mp); PyAPI_FUNC(int) PyDict_Next( diff --git a/Lib/test/test_weakref.py b/Lib/test/test_weakref.py index 9341c6d..43cf2c0 100644 --- a/Lib/test/test_weakref.py +++ b/Lib/test/test_weakref.py @@ -1676,6 +1676,18 @@ class MappingTestCase(TestBase): x = d.pop(10, 10) self.assertIsNot(x, None) # we never put None in there! + def test_threaded_weak_valued_consistency(self): + # Issue #28427: old keys should not remove new values from + # WeakValueDictionary when collecting from another thread. + d = weakref.WeakValueDictionary() + with collect_in_thread(): + for i in range(200000): + o = RefCycle() + d[10] = o + # o is still alive, so the dict can't be empty + self.assertEqual(len(d), 1) + o = None # lose ref + from test import mapping_tests diff --git a/Lib/weakref.py b/Lib/weakref.py index 9f8ef3e..aaebd0c 100644 --- a/Lib/weakref.py +++ b/Lib/weakref.py @@ -16,7 +16,8 @@ from _weakref import ( proxy, CallableProxyType, ProxyType, - ReferenceType) + ReferenceType, + _remove_dead_weakref) from _weakrefset import WeakSet, _IterationGuard @@ -111,7 +112,9 @@ class WeakValueDictionary(collections.MutableMapping): if self._iterating: self._pending_removals.append(wr.key) else: - del self.data[wr.key] + # Atomic removal is necessary since this function + # can be called asynchronously by the GC + _remove_dead_weakref(d, wr.key) self._remove = remove # A list of keys to be removed self._pending_removals = [] @@ -125,9 +128,12 @@ class WeakValueDictionary(collections.MutableMapping): # We shouldn't encounter any KeyError, because this method should # always be called *before* mutating the dict. while l: - del d[l.pop()] + key = l.pop() + _remove_dead_weakref(d, key) def __getitem__(self, key): + if self._pending_removals: + self._commit_removals() o = self.data[key]() if o is None: raise KeyError(key) @@ -140,9 +146,13 @@ class WeakValueDictionary(collections.MutableMapping): del self.data[key] def __len__(self): - return len(self.data) - len(self._pending_removals) + if self._pending_removals: + self._commit_removals() + return len(self.data) def __contains__(self, key): + if self._pending_removals: + self._commit_removals() try: o = self.data[key]() except KeyError: @@ -158,6 +168,8 @@ class WeakValueDictionary(collections.MutableMapping): self.data[key] = KeyedRef(value, self._remove, key) def copy(self): + if self._pending_removals: + self._commit_removals() new = WeakValueDictionary() for key, wr in self.data.items(): o = wr() @@ -169,6 +181,8 @@ class WeakValueDictionary(collections.MutableMapping): def __deepcopy__(self, memo): from copy import deepcopy + if self._pending_removals: + self._commit_removals() new = self.__class__() for key, wr in self.data.items(): o = wr() @@ -177,6 +191,8 @@ class WeakValueDictionary(collections.MutableMapping): return new def get(self, key, default=None): + if self._pending_removals: + self._commit_removals() try: wr = self.data[key] except KeyError: @@ -190,6 +206,8 @@ class WeakValueDictionary(collections.MutableMapping): return o def items(self): + if self._pending_removals: + self._commit_removals() with _IterationGuard(self): for k, wr in self.data.items(): v = wr() @@ -197,6 +215,8 @@ class WeakValueDictionary(collections.MutableMapping): yield k, v def keys(self): + if self._pending_removals: + self._commit_removals() with _IterationGuard(self): for k, wr in self.data.items(): if wr() is not None: @@ -214,10 +234,14 @@ class WeakValueDictionary(collections.MutableMapping): keep the values around longer than needed. """ + if self._pending_removals: + self._commit_removals() with _IterationGuard(self): yield from self.data.values() def values(self): + if self._pending_removals: + self._commit_removals() with _IterationGuard(self): for wr in self.data.values(): obj = wr() @@ -290,6 +314,8 @@ class WeakValueDictionary(collections.MutableMapping): keep the values around longer than needed. """ + if self._pending_removals: + self._commit_removals() return list(self.data.values()) @@ -208,6 +208,9 @@ Core and Builtins Library ------- +- Issue #28427: old keys should not remove new values from + WeakValueDictionary when collecting from another thread. + - Issue 28923: Remove editor artifacts from Tix.py. - Issue #28871: Fixed a crash when deallocate deep ElementTree. diff --git a/Modules/_weakref.c b/Modules/_weakref.c index 805d6d0..f9c68d6 100644 --- a/Modules/_weakref.c +++ b/Modules/_weakref.c @@ -35,6 +35,46 @@ _weakref_getweakrefcount_impl(PyObject *module, PyObject *object) } +static int +is_dead_weakref(PyObject *value) +{ + if (!PyWeakref_Check(value)) { + PyErr_SetString(PyExc_TypeError, "not a weakref"); + return -1; + } + return PyWeakref_GET_OBJECT(value) == Py_None; +} + +/*[clinic input] + +_weakref._remove_dead_weakref -> object + + dct: object(subclass_of='&PyDict_Type') + key: object + / + +Atomically remove key from dict if it points to a dead weakref. +[clinic start generated code]*/ + +static PyObject * +_weakref__remove_dead_weakref_impl(PyObject *module, PyObject *dct, + PyObject *key) +/*[clinic end generated code: output=d9ff53061fcb875c input=19fc91f257f96a1d]*/ +{ + if (_PyDict_DelItemIf(dct, key, is_dead_weakref) < 0) { + if (PyErr_ExceptionMatches(PyExc_KeyError)) + /* This function is meant to allow safe weak-value dicts + with GC in another thread (see issue #28427), so it's + ok if the key doesn't exist anymore. + */ + PyErr_Clear(); + else + return NULL; + } + Py_RETURN_NONE; +} + + PyDoc_STRVAR(weakref_getweakrefs__doc__, "getweakrefs(object) -- return a list of all weak reference objects\n" "that point to 'object'."); @@ -88,6 +128,7 @@ weakref_proxy(PyObject *self, PyObject *args) static PyMethodDef weakref_functions[] = { _WEAKREF_GETWEAKREFCOUNT_METHODDEF + _WEAKREF__REMOVE_DEAD_WEAKREF_METHODDEF {"getweakrefs", weakref_getweakrefs, METH_O, weakref_getweakrefs__doc__}, {"proxy", weakref_proxy, METH_VARARGS, diff --git a/Modules/clinic/_weakref.c.h b/Modules/clinic/_weakref.c.h index c192e72..ab84c30 100644 --- a/Modules/clinic/_weakref.c.h +++ b/Modules/clinic/_weakref.c.h @@ -29,4 +29,34 @@ _weakref_getweakrefcount(PyObject *module, PyObject *object) exit: return return_value; } -/*[clinic end generated code: output=e1ad587147323e19 input=a9049054013a1b77]*/ + +PyDoc_STRVAR(_weakref__remove_dead_weakref__doc__, +"_remove_dead_weakref($module, dct, key, /)\n" +"--\n" +"\n" +"Atomically remove key from dict if it points to a dead weakref."); + +#define _WEAKREF__REMOVE_DEAD_WEAKREF_METHODDEF \ + {"_remove_dead_weakref", (PyCFunction)_weakref__remove_dead_weakref, METH_VARARGS, _weakref__remove_dead_weakref__doc__}, + +static PyObject * +_weakref__remove_dead_weakref_impl(PyObject *module, PyObject *dct, + PyObject *key); + +static PyObject * +_weakref__remove_dead_weakref(PyObject *module, PyObject *args) +{ + PyObject *return_value = NULL; + PyObject *dct; + PyObject *key; + + if (!PyArg_ParseTuple(args, "O!O:_remove_dead_weakref", + &PyDict_Type, &dct, &key)) { + goto exit; + } + return_value = _weakref__remove_dead_weakref_impl(module, dct, key); + +exit: + return return_value; +} +/*[clinic end generated code: output=e860dd818a44bc9b input=a9049054013a1b77]*/ diff --git a/Objects/dictobject.c b/Objects/dictobject.c index 6a7e831..baef589 100644 --- a/Objects/dictobject.c +++ b/Objects/dictobject.c @@ -1583,11 +1583,32 @@ _PyDict_SetItem_KnownHash(PyObject *op, PyObject *key, PyObject *value, return insertdict(mp, key, hash, value); } +static int +delitem_common(PyDictObject *mp, Py_ssize_t hashpos, Py_ssize_t ix, + PyObject *old_value) +{ + PyObject *old_key; + PyDictKeyEntry *ep; + + mp->ma_used--; + mp->ma_version_tag = DICT_NEXT_VERSION(); + ep = &DK_ENTRIES(mp->ma_keys)[ix]; + dk_set_index(mp->ma_keys, hashpos, DKIX_DUMMY); + ENSURE_ALLOWS_DELETIONS(mp); + old_key = ep->me_key; + ep->me_key = NULL; + ep->me_value = NULL; + Py_DECREF(old_key); + Py_DECREF(old_value); + + assert(_PyDict_CheckConsistency(mp)); + return 0; +} + int PyDict_DelItem(PyObject *op, PyObject *key) { Py_hash_t hash; - assert(key); if (!PyUnicode_CheckExact(key) || (hash = ((PyASCIIObject *) key)->hash) == -1) { @@ -1604,8 +1625,7 @@ _PyDict_DelItem_KnownHash(PyObject *op, PyObject *key, Py_hash_t hash) { Py_ssize_t hashpos, ix; PyDictObject *mp; - PyDictKeyEntry *ep; - PyObject *old_key, *old_value; + PyObject *old_value; if (!PyDict_Check(op)) { PyErr_BadInternalCall(); @@ -1632,22 +1652,60 @@ _PyDict_DelItem_KnownHash(PyObject *op, PyObject *key, Py_hash_t hash) assert(ix >= 0); } - assert(old_value != NULL); - mp->ma_used--; - mp->ma_version_tag = DICT_NEXT_VERSION(); - ep = &DK_ENTRIES(mp->ma_keys)[ix]; - dk_set_index(mp->ma_keys, hashpos, DKIX_DUMMY); - ENSURE_ALLOWS_DELETIONS(mp); - old_key = ep->me_key; - ep->me_key = NULL; - ep->me_value = NULL; - Py_DECREF(old_key); - Py_DECREF(old_value); + return delitem_common(mp, hashpos, ix, old_value); +} - assert(_PyDict_CheckConsistency(mp)); - return 0; +/* This function promises that the predicate -> deletion sequence is atomic + * (i.e. protected by the GIL), assuming the predicate itself doesn't + * release the GIL. + */ +int +_PyDict_DelItemIf(PyObject *op, PyObject *key, + int (*predicate)(PyObject *value)) +{ + Py_ssize_t hashpos, ix; + PyDictObject *mp; + Py_hash_t hash; + PyObject *old_value; + int res; + + if (!PyDict_Check(op)) { + PyErr_BadInternalCall(); + return -1; + } + assert(key); + hash = PyObject_Hash(key); + if (hash == -1) + return -1; + mp = (PyDictObject *)op; + ix = (mp->ma_keys->dk_lookup)(mp, key, hash, &old_value, &hashpos); + if (ix == DKIX_ERROR) + return -1; + if (ix == DKIX_EMPTY || old_value == NULL) { + _PyErr_SetKeyError(key); + return -1; + } + assert(dk_get_index(mp->ma_keys, hashpos) == ix); + + // Split table doesn't allow deletion. Combine it. + if (_PyDict_HasSplitTable(mp)) { + if (dictresize(mp, DK_SIZE(mp->ma_keys))) { + return -1; + } + ix = (mp->ma_keys->dk_lookup)(mp, key, hash, &old_value, &hashpos); + assert(ix >= 0); + } + + res = predicate(old_value); + if (res == -1) + return -1; + if (res > 0) + return delitem_common(mp, hashpos, ix, old_value); + else + return 0; } + void PyDict_Clear(PyObject *op) { |