Skip to content

Commit d7e7cb4

Browse files
committed
gh-161143: Release completed buffered Pool iterator registrations
1 parent 4b21d15 commit d7e7cb4

3 files changed

Lines changed: 34 additions & 7 deletions

File tree

‎Lib/multiprocessing/pool.py‎

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -711,8 +711,9 @@ def _terminate_pool(cls, taskqueue, inqueue, outqueue, pool, change_notifier,
711711
change_notifier.put(None)
712712

713713
task_handler._state = TERMINATE
714-
# Release all semaphores to wake up task_handler to stop.
715-
for buffersize_sema in tuple(taskqueue_buffersize_semaphores):
714+
# Snapshot the set before releasing semaphores: the result handler can
715+
# remove completed ones concurrently.
716+
for buffersize_sema in list(taskqueue_buffersize_semaphores):
716717
buffersize_sema.release()
717718
taskqueue_buffersize_semaphores.discard(buffersize_sema)
718719

@@ -935,11 +936,13 @@ def next(self, timeout=None):
935936
return value
936937
raise value
937938

938-
def _stop_iterator(self):
939+
def _release_pool(self):
939940
if self._pool is not None:
940-
# `self._pool` could be set to `None` in previous `.next()` calls
941941
self._pool._taskqueue_buffersize_semaphores.discard(self._buffersize_sema)
942942
self._pool = None
943+
944+
def _stop_iterator(self):
945+
self._release_pool()
943946
raise StopIteration from None
944947

945948
__next__ = next # XXX
@@ -959,15 +962,15 @@ def _set(self, i, obj):
959962

960963
if self._index == self._length:
961964
del self._cache[self._job]
962-
self._pool = None
965+
self._release_pool()
963966

964967
def _set_length(self, length):
965968
with self._cond:
966969
self._length = length
967970
if self._index == self._length:
968971
self._cond.notify()
969972
del self._cache[self._job]
970-
self._pool = None
973+
self._release_pool()
971974

972975
#
973976
# Class whose instances are returned by `Pool.imap_unordered()`
@@ -982,7 +985,7 @@ def _set(self, i, obj):
982985
self._cond.notify()
983986
if self._index == self._length:
984987
del self._cache[self._job]
985-
self._pool = None
988+
self._release_pool()
986989

987990
#
988991
#

‎Lib/test/_test_multiprocessing.py‎

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3276,6 +3276,27 @@ def test_imap_and_imap_unordered_with_buffersize_on_empty_iterable(
32763276
self.assertIsNone(next(res, None))
32773277
self.assertIsNone(next(res, None))
32783278

3279+
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
3280+
def test_imap_with_buffersize_releases_semaphores(self):
3281+
if self.TYPE == 'manager':
3282+
self.skipTest('test requires access to the pool')
3283+
3284+
p = self.Pool(1)
3285+
try:
3286+
for method_name in ('imap', 'imap_unordered'):
3287+
for chunksize in (1, 2):
3288+
for values in ([], [1, 2, 3]):
3289+
with self.subTest(method=method_name,
3290+
chunksize=chunksize, values=values):
3291+
method = getattr(p, method_name)
3292+
result = method(str, values, chunksize=chunksize,
3293+
buffersize=1)
3294+
self.assertCountEqual(result, map(str, values))
3295+
self.assertFalse(p._taskqueue_buffersize_semaphores)
3296+
finally:
3297+
p.terminate()
3298+
p.join()
3299+
32793300
@warnings_helper.ignore_fork_in_thread_deprecation_warnings()
32803301
def test_imap_with_buffersize_on_infinite_iterable(self):
32813302
if self.TYPE != "threads":
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Fix a memory leak in :meth:`multiprocessing.pool.Pool.imap` and
2+
:meth:`~multiprocessing.pool.Pool.imap_unordered` when the *buffersize*
3+
parameter is used.

0 commit comments

Comments
 (0)