Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit ea76791

Browse files
authored
[3.6] bpo-27144: concurrent.futures as_complete and map iterators do not keep reference to returned object (GH-1560) (#3266)
bpo-27144: concurrent.futures as_complie and map iterators do not keep reference to returned object (cherry picked from commit 97e1b1c)
1 parent 98c849a commit ea76791

File tree

4 files changed

+91
-10
lines changed

4 files changed

+91
-10
lines changed

Lib/concurrent/futures/_base.py

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -170,6 +170,20 @@ def _create_and_install_waiters(fs, return_when):
170170

171171
return waiter
172172

173+
174+
def _yield_and_decref(fs, ref_collect):
175+
"""
176+
Iterate on the list *fs*, yielding objects one by one in reverse order.
177+
Before yielding an object, it is removed from each set in
178+
the collection of sets *ref_collect*.
179+
"""
180+
while fs:
181+
for futures_set in ref_collect:
182+
futures_set.remove(fs[-1])
183+
# Careful not to keep a reference to the popped value
184+
yield fs.pop()
185+
186+
173187
def as_completed(fs, timeout=None):
174188
"""An iterator over the given futures that yields each as it completes.
175189
@@ -191,16 +205,18 @@ def as_completed(fs, timeout=None):
191205
if timeout is not None:
192206
end_time = timeout + time.time()
193207

208+
total_futures = len(fs)
209+
194210
fs = set(fs)
195211
with _AcquireFutures(fs):
196212
finished = set(
197213
f for f in fs
198214
if f._state in [CANCELLED_AND_NOTIFIED, FINISHED])
199215
pending = fs - finished
200216
waiter = _create_and_install_waiters(fs, _AS_COMPLETED)
201-
217+
finished = list(finished)
202218
try:
203-
yield from finished
219+
yield from _yield_and_decref(finished, ref_collect=(fs,))
204220

205221
while pending:
206222
if timeout is None:
@@ -210,7 +226,7 @@ def as_completed(fs, timeout=None):
210226
if wait_timeout < 0:
211227
raise TimeoutError(
212228
'%d (of %d) futures unfinished' % (
213-
len(pending), len(fs)))
229+
len(pending), total_futures))
214230

215231
waiter.event.wait(wait_timeout)
216232

@@ -219,9 +235,9 @@ def as_completed(fs, timeout=None):
219235
waiter.finished_futures = []
220236
waiter.event.clear()
221237

222-
for future in finished:
223-
yield future
224-
pending.remove(future)
238+
# reverse to keep finishing order
239+
finished.reverse()
240+
yield from _yield_and_decref(finished, ref_collect=(fs, pending))
225241

226242
finally:
227243
for f in fs:
@@ -551,11 +567,14 @@ def map(self, fn, *iterables, timeout=None, chunksize=1):
551567
# before the first iterator value is required.
552568
def result_iterator():
553569
try:
554-
for future in fs:
570+
# reverse to keep finishing order
571+
fs.reverse()
572+
while fs:
573+
# Careful not to keep a reference to the popped future
555574
if timeout is None:
556-
yield future.result()
575+
yield fs.pop().result()
557576
else:
558-
yield future.result(end_time - time.time())
577+
yield fs.pop().result(end_time - time.time())
559578
finally:
560579
for future in fs:
561580
future.cancel()

Lib/concurrent/futures/process.py

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,6 +357,18 @@ def _check_system_limits():
357357
raise NotImplementedError(_system_limited)
358358

359359

360+
def _chain_from_iterable_of_lists(iterable):
361+
"""
362+
Specialized implementation of itertools.chain.from_iterable.
363+
Each item in *iterable* should be a list. This function is
364+
careful not to keep references to yielded objects.
365+
"""
366+
for element in iterable:
367+
element.reverse()
368+
while element:
369+
yield element.pop()
370+
371+
360372
class BrokenProcessPool(RuntimeError):
361373
"""
362374
Raised when a process in a ProcessPoolExecutor terminated abruptly
@@ -482,7 +494,7 @@ def map(self, fn, *iterables, timeout=None, chunksize=1):
482494
results = super().map(partial(_process_chunk, fn),
483495
_get_chunks(*iterables, chunksize=chunksize),
484496
timeout=timeout)
485-
return itertools.chain.from_iterable(results)
497+
return _chain_from_iterable_of_lists(results)
486498

487499
def shutdown(self, wait=True):
488500
with self._shutdown_lock:

Lib/test/test_concurrent_futures.py

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,10 @@ def my_method(self):
5959
pass
6060

6161

62+
def make_dummy_object(_):
63+
return MyObject()
64+
65+
6266
class BaseTestCase(unittest.TestCase):
6367
def setUp(self):
6468
self._thread_key = test.support.threading_setup()
@@ -397,6 +401,38 @@ def test_duplicate_futures(self):
397401
completed = [f for f in futures.as_completed([future1,future1])]
398402
self.assertEqual(len(completed), 1)
399403

404+
def test_free_reference_yielded_future(self):
405+
# Issue #14406: Generator should not keep references
406+
# to finished futures.
407+
futures_list = [Future() for _ in range(8)]
408+
futures_list.append(create_future(state=CANCELLED_AND_NOTIFIED))
409+
futures_list.append(create_future(state=SUCCESSFUL_FUTURE))
410+
411+
with self.assertRaises(futures.TimeoutError):
412+
for future in futures.as_completed(futures_list, timeout=0):
413+
futures_list.remove(future)
414+
wr = weakref.ref(future)
415+
del future
416+
self.assertIsNone(wr())
417+
418+
futures_list[0].set_result("test")
419+
for future in futures.as_completed(futures_list):
420+
futures_list.remove(future)
421+
wr = weakref.ref(future)
422+
del future
423+
self.assertIsNone(wr())
424+
if futures_list:
425+
futures_list[0].set_result("test")
426+
427+
def test_correct_timeout_exception_msg(self):
428+
futures_list = [CANCELLED_AND_NOTIFIED_FUTURE, PENDING_FUTURE,
429+
RUNNING_FUTURE, SUCCESSFUL_FUTURE]
430+
431+
with self.assertRaises(futures.TimeoutError) as cm:
432+
list(futures.as_completed(futures_list, timeout=0))
433+
434+
self.assertEqual(str(cm.exception), '2 (of 4) futures unfinished')
435+
400436

401437
class ThreadPoolAsCompletedTests(ThreadPoolMixin, AsCompletedTests, BaseTestCase):
402438
pass
@@ -422,6 +458,10 @@ def test_map(self):
422458
list(self.executor.map(pow, range(10), range(10))),
423459
list(map(pow, range(10), range(10))))
424460

461+
self.assertEqual(
462+
list(self.executor.map(pow, range(10), range(10), chunksize=3)),
463+
list(map(pow, range(10), range(10))))
464+
425465
def test_map_exception(self):
426466
i = self.executor.map(divmod, [1, 1, 1, 1], [2, 3, 0, 5])
427467
self.assertEqual(i.__next__(), (0, 1))
@@ -472,6 +512,14 @@ def test_max_workers_negative(self):
472512
"than 0"):
473513
self.executor_type(max_workers=number)
474514

515+
def test_free_reference(self):
516+
# Issue #14406: Result iterator should not keep an internal
517+
# reference to result objects.
518+
for obj in self.executor.map(make_dummy_object, range(10)):
519+
wr = weakref.ref(obj)
520+
del obj
521+
self.assertIsNone(wr())
522+
475523

476524
class ThreadPoolExecutorTest(ThreadPoolMixin, ExecutorTest, BaseTestCase):
477525
def test_map_submits_without_iteration(self):
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
The ``map()`` and ``as_completed()`` iterators in ``concurrent.futures``
2+
now avoid keeping a reference to yielded objects.

0 commit comments

Comments
 (0)