diff --git a/Doc/library/concurrent.futures.rst b/Doc/library/concurrent.futures.rst index d3c7a40aa9d390..481615218c9359 100644 --- a/Doc/library/concurrent.futures.rst +++ b/Doc/library/concurrent.futures.rst @@ -51,11 +51,18 @@ Executor Objects The returned iterator raises a :exc:`TimeoutError` if :meth:`~iterator.__next__` is called and the result isn't available after *timeout* seconds from the original call to :meth:`Executor.map`. - *timeout* can be an int or a float. If *timeout* is not specified or + *timeout* can be an int or a float. + It cancels all future calls of *fn* and closes the iterator. + If *timeout* is not specified or ``None``, there is no limit to the wait time. If a *fn* call raises an exception, then that exception will be raised when its value is retrieved from the iterator. + It does not cancel future calls of *fn*. + + The returned iterator has method :meth:`!close` which cancels all + future calls of *fn* and discards the results of already finished calls + if they are available. When using :class:`ProcessPoolExecutor`, this method chops *iterables* into a number of chunks which it submits to the pool as separate @@ -68,6 +75,10 @@ Executor Objects .. versionchanged:: 3.5 Added the *chunksize* argument. + .. versionchanged:: 3.13 + The returned iterator no longer automatically closed if a *fn* call + raises an exception. + .. method:: shutdown(wait=True, *, cancel_futures=False) Signal the executor that it should free any resources that it is using diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index f17c6ec0775bef..258dd7ba35f5bc 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -144,6 +144,11 @@ Other Language Changes (Contributed by Levi Sabah, Zackery Spytz and Hugo van Kemenade in :gh:`73965`.) +* The iterator returned by :meth:`concurrent.futures.Executor.map` is no longer + automatically closed if a function call raises an exception. + Use method :meth:`!close` to explicitly close the iterator. + (Contributed by xzmeng and Serhiy Storchaka in :gh:`108518`.) + New Modules =========== diff --git a/Lib/concurrent/futures/_base.py b/Lib/concurrent/futures/_base.py index 6742a07753c921..b618f70a0e3e2b 100644 --- a/Lib/concurrent/futures/_base.py +++ b/Lib/concurrent/futures/_base.py @@ -314,7 +314,11 @@ def wait(fs, timeout=None, return_when=ALL_COMPLETED): def _result_or_cancel(fut, timeout=None): try: try: - return fut.result(timeout) + return (fut.result(timeout), None) + except TimeoutError: + raise + except BaseException as exc: + return (None, exc) finally: fut.cancel() finally: @@ -566,6 +570,7 @@ def set_exception(self, exception): __class_getitem__ = classmethod(types.GenericAlias) + class Executor(object): """This is an abstract base class for concrete asynchronous executors.""" @@ -602,6 +607,9 @@ def map(self, fn, *iterables, timeout=None, chunksize=1): before the given timeout. Exception: If fn(*args) raises for any values. """ + return _MapResultIterator(self._map(fn, *iterables, timeout=timeout)) + + def _map(self, fn, *iterables, timeout=None): if timeout is not None: end_time = timeout + time.monotonic() @@ -648,6 +656,24 @@ def __exit__(self, exc_type, exc_val, exc_tb): return False +class _MapResultIterator: + """The iterator returned by map().""" + def __init__(self, gen): + self.gen = gen + + def __iter__(self): + return self + + def __next__(self): + value, exc = next(self.gen) + if exc is not None: + raise exc + return value + + def close(self): + self.gen.close() + + class BrokenExecutor(RuntimeError): """ Raised when a executor has become non-functional after a severe failure. diff --git a/Lib/concurrent/futures/process.py b/Lib/concurrent/futures/process.py index ca843e11eeb83d..6fbd7b0c15c3cf 100644 --- a/Lib/concurrent/futures/process.py +++ b/Lib/concurrent/futures/process.py @@ -199,7 +199,14 @@ def _process_chunk(fn, chunk): This function is run in a separate process. """ - return [fn(*args) for args in chunk] + results = [] + for args in chunk: + try: + result = (fn(*args), None) + except BaseException as exc: + result = (None, exc) + results.append(result) + return results def _sendback_result(result_queue, work_id, result=None, exception=None, @@ -839,7 +846,7 @@ def map(self, fn, *iterables, timeout=None, chunksize=1): results = super().map(partial(_process_chunk, fn), itertools.batched(zip(*iterables), chunksize), timeout=timeout) - return _chain_from_iterable_of_lists(results) + return _base._MapResultIterator(_chain_from_iterable_of_lists(results)) def shutdown(self, wait=True, *, cancel_futures=False): with self._shutdown_lock: diff --git a/Lib/test/test_concurrent_futures/executor.py b/Lib/test/test_concurrent_futures/executor.py index 1e7d4344740943..f864bf43ac1254 100644 --- a/Lib/test/test_concurrent_futures/executor.py +++ b/Lib/test/test_concurrent_futures/executor.py @@ -48,25 +48,43 @@ def test_map(self): list(map(pow, range(10), range(10)))) def test_map_exception(self): - i = self.executor.map(divmod, [1, 1, 1, 1], [2, 3, 0, 5]) - self.assertEqual(i.__next__(), (0, 1)) - self.assertEqual(i.__next__(), (0, 1)) - self.assertRaises(ZeroDivisionError, i.__next__) + i = self.executor.map(divmod, [5, 5, 5, 5], [2, 3, 0, 5]) + self.assertEqual(next(i), (2, 1)) + self.assertEqual(next(i), (1, 2)) + self.assertRaises(ZeroDivisionError, next, i) + self.assertEqual(next(i), (1, 0)) + self.assertRaises(StopIteration, next, i) + self.assertRaises(StopIteration, next, i) + + i = self.executor.map(divmod, [5, 5, 5, 5], [2, 0, 3, 5], chunksize=3) + self.assertEqual(next(i), (2, 1)) + self.assertRaises(ZeroDivisionError, next, i) + self.assertEqual(next(i), (1, 2)) + self.assertEqual(next(i), (1, 0)) + self.assertRaises(StopIteration, next, i) + self.assertRaises(StopIteration, next, i) @support.requires_resource('walltime') def test_map_timeout(self): - results = [] - try: - for i in self.executor.map(time.sleep, - [0, 0, 6], - timeout=5): - results.append(i) - except futures.TimeoutError: - pass - else: - self.fail('expected TimeoutError') - - self.assertEqual([None, None], results) + i = self.executor.map(time.sleep, [0, 0, 6, 0], timeout=5) + next(i) + next(i) + self.assertRaises(futures.TimeoutError, next, i) + self.assertRaises(StopIteration, next, i) + self.assertRaises(StopIteration, next, i) + + def test_map_close(self): + i = self.executor.map(divmod, [5, 5, 5, 5], [2, 0, 3, 5]) + self.assertEqual(next(i), (2, 1)) + i.close() + self.assertRaises(StopIteration, next, i) + self.assertRaises(StopIteration, next, i) + + i = self.executor.map(divmod, [5, 5, 5, 5], [2, 0, 3, 5], chunksize=3) + self.assertEqual(next(i), (2, 1)) + i.close() + self.assertRaises(StopIteration, next, i) + self.assertRaises(StopIteration, next, i) def test_shutdown_race_issue12456(self): # Issue #12456: race condition at shutdown where trying to post a diff --git a/Misc/NEWS.d/next/Library/2024-02-04-13-56-48.gh-issue-108518.6NCPk_.rst b/Misc/NEWS.d/next/Library/2024-02-04-13-56-48.gh-issue-108518.6NCPk_.rst new file mode 100644 index 00000000000000..d7cf5ba88fa186 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-02-04-13-56-48.gh-issue-108518.6NCPk_.rst @@ -0,0 +1,3 @@ +The iterator returned by :meth:`concurrent.futures.Executor.map` is no longer +automatically closed if a function call raises an exception. +Use method :meth:`!close` to explicitly close the iterator. 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