Issue #19542: Fix bugs in WeakValueDictionary.setdefault() and WeakValueDictionary.pop()

when a GC collection happens in another thread.

Original patch and report by Armin Rigo.
This commit is contained in:
Antoine Pitrou 2016-12-19 10:59:15 +01:00
commit ec60edb977
3 changed files with 53 additions and 5 deletions

View File

@ -6,6 +6,7 @@ import weakref
import operator import operator
import contextlib import contextlib
import copy import copy
import time
from test import support from test import support
from test.support import script_helper from test.support import script_helper
@ -72,6 +73,29 @@ class TestBase(unittest.TestCase):
self.cbcalled += 1 self.cbcalled += 1
@contextlib.contextmanager
def collect_in_thread(period=0.0001):
"""
Ensure GC collections happen in a different thread, at a high frequency.
"""
threading = support.import_module('threading')
please_stop = False
def collect():
while not please_stop:
time.sleep(period)
gc.collect()
with support.disable_gc():
t = threading.Thread(target=collect)
t.start()
try:
yield
finally:
please_stop = True
t.join()
class ReferencesTestCase(TestBase): class ReferencesTestCase(TestBase):
def test_basic_ref(self): def test_basic_ref(self):
@ -1636,6 +1660,23 @@ class MappingTestCase(TestBase):
dict = weakref.WeakKeyDictionary() dict = weakref.WeakKeyDictionary()
self.assertRegex(repr(dict), '<WeakKeyDictionary at 0x.*>') self.assertRegex(repr(dict), '<WeakKeyDictionary at 0x.*>')
def test_threaded_weak_valued_setdefault(self):
d = weakref.WeakValueDictionary()
with collect_in_thread():
for i in range(100000):
x = d.setdefault(10, RefCycle())
self.assertIsNot(x, None) # we never put None in there!
del x
def test_threaded_weak_valued_pop(self):
d = weakref.WeakValueDictionary()
with collect_in_thread():
for i in range(100000):
d[10] = RefCycle()
x = d.pop(10, 10)
self.assertIsNot(x, None) # we never put None in there!
from test import mapping_tests from test import mapping_tests
class WeakValueDictionaryTestCase(mapping_tests.BasicTestMappingProtocol): class WeakValueDictionaryTestCase(mapping_tests.BasicTestMappingProtocol):

View File

@ -239,24 +239,27 @@ class WeakValueDictionary(collections.MutableMapping):
try: try:
o = self.data.pop(key)() o = self.data.pop(key)()
except KeyError: except KeyError:
o = None
if o is None:
if args: if args:
return args[0] return args[0]
raise else:
if o is None: raise KeyError(key)
raise KeyError(key)
else: else:
return o return o
def setdefault(self, key, default=None): def setdefault(self, key, default=None):
try: try:
wr = self.data[key] o = self.data[key]()
except KeyError: except KeyError:
o = None
if o is None:
if self._pending_removals: if self._pending_removals:
self._commit_removals() self._commit_removals()
self.data[key] = KeyedRef(default, self._remove, key) self.data[key] = KeyedRef(default, self._remove, key)
return default return default
else: else:
return wr() return o
def update(*args, **kwargs): def update(*args, **kwargs):
if not args: if not args:

View File

@ -200,6 +200,10 @@ Core and Builtins
Library Library
------- -------
- Issue #19542: Fix bugs in WeakValueDictionary.setdefault() and
WeakValueDictionary.pop() when a GC collection happens in another
thread.
- Issue #20191: Fixed a crash in resource.prlimit() when pass a sequence that - Issue #20191: Fixed a crash in resource.prlimit() when pass a sequence that
doesn't own its elements as limits. doesn't own its elements as limits.