Skip to content

gh-64192: Make imap()/imap_unordered() in multiprocessing.pool actually lazy #136871

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 21 commits into
base: main
Choose a base branch
from
Open
Changes from 1 commit
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d8e8a02
draft: impl lazy input consumption in mp.Pool.imap(_unordered)
obaltian Jul 20, 2025
f6f423c
Use semaphore to synchronize threads
obaltian Jul 20, 2025
937862d
Update buffersize behavior to match concurrent.futures.Executor behavior
obaltian Jul 21, 2025
b6f6caa
Release all `buffersize_lock` obj from the parent thread when terminate
obaltian Jul 21, 2025
3bafd5d
Add 2 basic `ThreadPool.imap()` tests w/ and w/o buffersize
obaltian Jul 21, 2025
e43232b
Fix accidental swap in imports
obaltian Jul 21, 2025
dd416e0
clear Pool._taskqueue_buffersize_semaphores safely
obaltian Jul 21, 2025
99f5a8c
Slightly optimize Pool._taskqueue_buffersize_semaphores terminate
obaltian Jul 21, 2025
2a53398
Rename `Pool.imap()` buffersize-related tests
obaltian Jul 21, 2025
f8878eb
Fix typo in `IMapIterator.__init__()`
obaltian Jul 22, 2025
2ca51e3
Add tests for buffersize combinations with other kwargs
obaltian Jul 22, 2025
bf27d5d
Remove if-branch in `_terminate_pool`
obaltian Jul 27, 2025
508c765
Add more edge-case tests for `imap` and `imap_unodered`
obaltian Jul 27, 2025
dff1167
Split inf iterable test for `imap` and `imap_unordered`
obaltian Jul 27, 2025
94cc0b9
Add doc for `buffersize` argument of `imap` and `imap_unordered`
obaltian Jul 27, 2025
816fb6c
add *versionadded* for `imap_unordered`
obaltian Jul 28, 2025
88cc10a
Remove ambiguity in `buffersize` description.
obaltian Jul 28, 2025
05e3b24
Set *versionadded* as next in docs
obaltian Jul 28, 2025
503982f
Add whatsnew entry
obaltian Jul 28, 2025
b92cad9
Fix aggreed comments on code formatting/minor refactoring
obaltian Jul 28, 2025
02ebc6a
Remove `imap` and `imap_unordered` body code duplication
obaltian Jul 28, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
Add more edge-case tests for imap and imap_unodered
These tests mostly come from a similar PR adding `buffersize` param
to `concurrent.futures.Executor.map` -
https://github.com/python/cpython/pull/125663/files
  • Loading branch information
obaltian committed Jul 27, 2025
commit 508c76551d62cbcd8ad587da85c01aebb142754d
180 changes: 122 additions & 58 deletions Lib/test/_test_multiprocessing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2953,64 +2953,6 @@ def test_imap(self):
self.assertEqual(next(it), i * i)
self.assertRaises(StopIteration, it.__next__)

def test_imap_fast_iterable_with_slow_task(self):
if self.TYPE != "threads":
self.skipTest("test not appropriate for {}".format(self.TYPE))

processes = 4
p = self.Pool(processes)

tasks_started_later = 2
last_produced_task_arg = Value("i")

def produce_args():
for arg in range(1, processes + tasks_started_later + 1):
last_produced_task_arg.value = arg
yield arg

it = p.imap(functools.partial(sqr, wait=0.2), produce_args())

next(it)
time.sleep(0.2)
# `iterable` should've been advanced only up by `processes` times,
# but in fact advances further (by `>=processes+1`).
# In this case, it advances to the maximum value.
self.assertGreater(last_produced_task_arg.value, processes + 1)

p.terminate()
p.join()

def test_imap_fast_iterable_with_slow_task_and_buffersize(self):
if self.TYPE != "threads":
self.skipTest("test not appropriate for {}".format(self.TYPE))

processes = 4
p = self.Pool(processes)

tasks_started_later = 2
last_produced_task_arg = Value("i")

def produce_args():
for arg in range(1, processes + tasks_started_later + 1):
last_produced_task_arg.value = arg
yield arg

it = p.imap(
functools.partial(sqr, wait=0.2),
produce_args(),
buffersize=processes,
)

time.sleep(0.2)
self.assertEqual(last_produced_task_arg.value, processes)

next(it)
time.sleep(0.2)
self.assertEqual(last_produced_task_arg.value, processes + 1)

p.terminate()
p.join()

def test_imap_handle_iterable_exception(self):
if self.TYPE == 'manager':
self.skipTest('test not appropriate for {}'.format(self.TYPE))
Expand Down Expand Up @@ -3101,6 +3043,128 @@ def test_imap_unordered_handle_iterable_exception(self):
self.assertIn(value, expected_values)
expected_values.remove(value)

def test_imap_and_imap_unordered_buffersize_type_validation(self):
for method_name in ("imap", "imap_unordered"):
for buffersize in ("foo", 2.0):
with (
self.subTest(method=method_name, buffersize=buffersize),
self.assertRaisesRegex(
TypeError, "buffersize must be an integer or None"
),
):
method = getattr(self.pool, method_name)
method(str, range(4), buffersize=buffersize)

def test_imap_and_imap_unordered_buffersize_value_validation(self):
for method_name in ("imap", "imap_unordered"):
for buffersize in (0, -1):
with (
self.subTest(method=method_name, buffersize=buffersize),
self.assertRaisesRegex(
ValueError, "buffersize must be None or > 0"
),
):
method = getattr(self.pool, method_name)
method(str, range(4), buffersize=buffersize)

def test_imap_and_imap_unordered_when_buffer_is_full(self):
if self.TYPE != "threads":
self.skipTest("test not appropriate for {}".format(self.TYPE))

for method_name in ("imap", "imap_unordered"):
with self.subTest(method=method_name):
processes = 4
p = self.Pool(processes)
last_produced_task_arg = Value("i")

def produce_args():
for arg in itertools.count(1):
last_produced_task_arg.value = arg
yield arg

method = getattr(p, method_name)
it = method(functools.partial(sqr, wait=0.2), produce_args())

time.sleep(0.2)
# `iterable` could've been advanced only `processes` times,
# but in fact it advances further (`> processes`) because of
# not waiting for workers or user code to catch up.
self.assertGreater(last_produced_task_arg.value, processes)

next(it)
time.sleep(0.2)
self.assertGreater(last_produced_task_arg.value, processes + 1)

next(it)
time.sleep(0.2)
self.assertGreater(last_produced_task_arg.value, processes + 2)

p.terminate()
p.join()

def test_imap_and_imap_unordered_buffersize_when_buffer_is_full(self):
if self.TYPE != "threads":
self.skipTest("test not appropriate for {}".format(self.TYPE))

for method_name in ("imap", "imap_unordered"):
with self.subTest(method=method_name):
processes = 4
p = self.Pool(processes)
last_produced_task_arg = Value("i")

def produce_args():
for arg in itertools.count(1):
last_produced_task_arg.value = arg
yield arg

method = getattr(p, method_name)
it = method(
functools.partial(sqr, wait=0.2),
produce_args(),
buffersize=processes,
)

time.sleep(0.2)
self.assertEqual(last_produced_task_arg.value, processes)

next(it)
time.sleep(0.2)
self.assertEqual(last_produced_task_arg.value, processes + 1)

next(it)
time.sleep(0.2)
self.assertEqual(last_produced_task_arg.value, processes + 2)

p.terminate()
p.join()

def test_imap_and_imap_unordered_buffersize_on_infinite_iterable(self):
if self.TYPE != "threads":
self.skipTest("test not appropriate for {}".format(self.TYPE))

for method_name in ("imap", "imap_unordered"):
with self.subTest(method=method_name):
p = self.Pool(4)
method = getattr(p, method_name)

res = method(str, itertools.count(), buffersize=2)

self.assertEqual(next(res, None), "0")
self.assertEqual(next(res, None), "1")
self.assertEqual(next(res, None), "2")

p.terminate()
p.join()

def test_imap_and_imap_unordered_buffersize_on_empty_iterable(self):
for method_name in ("imap", "imap_unordered"):
with self.subTest(method=method_name):
method = getattr(self.pool, method_name)

res = method(str, [], buffersize=2)

self.assertIsNone(next(res, None))

def test_make_pool(self):
expected_error = (RemoteError if self.TYPE == 'manager'
else ValueError)
Expand Down
pFad - Phonifier reborn

Pfad - The Proxy pFad of © 2024 Garber Painting. All rights reserved.

Note: This service is not intended for secure transactions such as banking, social media, email, or purchasing. Use at your own risk. We assume no liability whatsoever for broken pages.


Alternative Proxies:

Alternative Proxy

pFad Proxy

pFad v3 Proxy

pFad v4 Proxy