diff --git a/Lib/multiprocessing/util.py b/Lib/multiprocessing/util.py index 0c4eb2473273b4..a036d72cc2f60e 100644 --- a/Lib/multiprocessing/util.py +++ b/Lib/multiprocessing/util.py @@ -348,8 +348,11 @@ def __exit__(self, *args): class ForkAwareLocal(threading.local): - def __init__(self): - register_after_fork(self, lambda obj : obj.__dict__.clear()) + def __new__(cls): + self = threading.local.__new__(cls) + register_after_fork(self, lambda obj: obj.__dict__.clear()) + return self + def __reduce__(self): return type(self), () diff --git a/Lib/test/_test_multiprocessing.py b/Lib/test/_test_multiprocessing.py index f4239badfe8b7e..fcdf9f4d97081a 100644 --- a/Lib/test/_test_multiprocessing.py +++ b/Lib/test/_test_multiprocessing.py @@ -4598,6 +4598,27 @@ def test_lock(self): join_process(p) self.assertLessEqual(new_size, old_size) +# +# Issue #26180: ForkAwareLocal +# + +class TestForkAwareLocal(unittest.TestCase): + # Issue #26180 meant that with each new thread using an instance of + # ForkAwareLocal, fork registry would get a duplicate entry for that + # instance. + + def test_registry_after_thread_spawn(self): + v = util.ForkAwareLocal() + # v cleanup handler is now registered. + old_size = len(util._afterfork_registry) + # Setting an attr on v from a new thread will call v.__init__. + t = threading.Thread(target=setattr, args=(v, 'x', 0)) + t.start() + t.join() + # Ensure that v.__init__ did not register a new handler. + new_size = len(util._afterfork_registry) + self.assertEqual(new_size, old_size) + # # Check that non-forked child processes do not inherit unneeded fds/handles # diff --git a/Misc/NEWS.d/next/Library/2019-06-11-22-58-11.bpo-26180.yXZmdk.rst b/Misc/NEWS.d/next/Library/2019-06-11-22-58-11.bpo-26180.yXZmdk.rst new file mode 100644 index 00000000000000..9b59fd9a13bc12 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2019-06-11-22-58-11.bpo-26180.yXZmdk.rst @@ -0,0 +1,2 @@ +Fixed memory leak in :class:`multiprocessing.util.ForkAwareLocal` when used +by multiple threads.