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

Skip to content

Commit b58f053

Browse files
committed
asyncio: Change as_completed() to use a Queue, to avoid O(N**2) behavior. Fixes issue #20566.
1 parent ee6dc42 commit b58f053

2 files changed

Lines changed: 55 additions & 21 deletions

File tree

Lib/asyncio/tasks.py

Lines changed: 33 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -463,16 +463,20 @@ def _on_completion(f):
463463

464464
# This is *not* a @coroutine! It is just an iterator (yielding Futures).
465465
def as_completed(fs, *, loop=None, timeout=None):
466-
"""Return an iterator whose values, when waited for, are Futures.
466+
"""Return an iterator whose values are coroutines.
467+
468+
When waiting for the yielded coroutines you'll get the results (or
469+
exceptions!) of the original Futures (or coroutines), in the order
470+
in which and as soon as they complete.
467471
468472
This differs from PEP 3148; the proper way to use this is:
469473
470474
for f in as_completed(fs):
471475
result = yield from f # The 'yield from' may raise.
472476
# Use result.
473477
474-
Raises TimeoutError if the timeout occurs before all Futures are
475-
done.
478+
If a timeout is specified, the 'yield from' will raise
479+
TimeoutError when the timeout occurs before all Futures are done.
476480
477481
Note: The futures 'f' are not necessarily members of fs.
478482
"""
@@ -481,27 +485,36 @@ def as_completed(fs, *, loop=None, timeout=None):
481485
loop = loop if loop is not None else events.get_event_loop()
482486
deadline = None if timeout is None else loop.time() + timeout
483487
todo = {async(f, loop=loop) for f in set(fs)}
484-
completed = collections.deque()
488+
from .queues import Queue # Import here to avoid circular import problem.
489+
done = Queue(loop=loop)
490+
timeout_handle = None
491+
492+
def _on_timeout():
493+
for f in todo:
494+
f.remove_done_callback(_on_completion)
495+
done.put_nowait(None) # Queue a dummy value for _wait_for_one().
496+
todo.clear() # Can't do todo.remove(f) in the loop.
497+
498+
def _on_completion(f):
499+
if not todo:
500+
return # _on_timeout() was here first.
501+
todo.remove(f)
502+
done.put_nowait(f)
503+
if not todo and timeout_handle is not None:
504+
timeout_handle.cancel()
485505

486506
@coroutine
487507
def _wait_for_one():
488-
while not completed:
489-
timeout = None
490-
if deadline is not None:
491-
timeout = deadline - loop.time()
492-
if timeout < 0:
493-
raise futures.TimeoutError()
494-
done, pending = yield from _wait(
495-
todo, timeout, FIRST_COMPLETED, loop)
496-
# Multiple callers might be waiting for the same events
497-
# and getting the same outcome. Dedupe by updating todo.
498-
for f in done:
499-
if f in todo:
500-
todo.remove(f)
501-
completed.append(f)
502-
f = completed.popleft()
503-
return f.result() # May raise.
508+
f = yield from done.get()
509+
if f is None:
510+
# Dummy value from _on_timeout().
511+
raise futures.TimeoutError
512+
return f.result() # May raise f.exception().
504513

514+
for f in todo:
515+
f.add_done_callback(_on_completion)
516+
if todo and timeout is not None:
517+
timeout_handle = loop.call_later(timeout, _on_timeout)
505518
for _ in range(len(todo)):
506519
yield _wait_for_one()
507520

Lib/test/test_asyncio/test_tasks.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,6 @@ def gen():
779779
yield 0
780780
yield 0
781781
yield 0.1
782-
yield 0.02
783782

784783
loop = test_utils.TestLoop(gen)
785784
self.addCleanup(loop.close)
@@ -791,6 +790,8 @@ def gen():
791790
def foo():
792791
values = []
793792
for f in asyncio.as_completed([a, b], timeout=0.12, loop=loop):
793+
if values:
794+
loop.advance_time(0.02)
794795
try:
795796
v = yield from f
796797
values.append((1, v))
@@ -809,6 +810,26 @@ def foo():
809810
loop.advance_time(10)
810811
loop.run_until_complete(asyncio.wait([a, b], loop=loop))
811812

813+
def test_as_completed_with_unused_timeout(self):
814+
815+
def gen():
816+
yield
817+
yield 0
818+
yield 0.01
819+
820+
loop = test_utils.TestLoop(gen)
821+
self.addCleanup(loop.close)
822+
823+
a = asyncio.sleep(0.01, 'a', loop=loop)
824+
825+
@asyncio.coroutine
826+
def foo():
827+
for f in asyncio.as_completed([a], timeout=1, loop=loop):
828+
v = yield from f
829+
self.assertEqual(v, 'a')
830+
831+
res = loop.run_until_complete(asyncio.Task(foo(), loop=loop))
832+
812833
def test_as_completed_reverse_wait(self):
813834

814835
def gen():

0 commit comments

Comments
 (0)