How to properly terminate nested asyncio-tasks

倖福魔咒の 提交于 2020-04-18 05:48:04

问题


I here asked how I can make a version of asyncio.gather that executes the list of tasks sequentially and not in parallel, and good people told me how. But my joy ended as soon as I tried to put two in_sequence() methods into one another:

import asyncio
from typing import Coroutine


async def work_a():
    print("Work 'A' start")
    await asyncio.sleep(0)
    print("Work 'A' finish")


async def work_b():
    print("Work 'B' start")
    await asyncio.sleep(0)
    print("Work 'B' finish")


async def raise_exception():
    print("raise_exception executed")
    await asyncio.sleep(0)
    raise RuntimeError("raise_exception executed")


async def in_sequence(*tasks: Coroutine):
    """Executes coroutines in sequence"""
    tasks = iter(tasks)
    try:
        for task in tasks:
            await task

    # Terminates all other tasks scheduled for execution in event loop to prevent "RuntimeWarning: coroutine was never awaited" message
    finally:
        for remaining_task in tasks:
            remaining_task.close()

async def main():
    try:
        await in_sequence(              # outer in_sequence()
            work_a(),
            raise_exception(),
            in_sequence(                # inner in_sequence()
                work_b()
            )
        )

    except Exception as e:
        print(f"+++ Quit with exception: {e}")


if __name__ == '__main__':
    asyncio.run(main())

As a result, I received the following output with "RuntimeWarning: coroutine 'work_b' was never awaited" error:

Work 'A' start
Work 'A' finish
raise_exception executed
+++ Quit with exception: raise_exception executed
{some path}\terminate_inner_coroutine_issue.py:33: RuntimeWarning: coroutine 'work_b' was never awaited
  remaining_task.close()
RuntimeWarning: Enable tracemalloc to get the object allocation traceback

After several experiments, I realized what the problem was: When the raise_exception() method throws an exception in the outer in_sequence() method, the outer in_sequence() method terminates only its child tasks in the finally block, i.e. only inner in_sequence(). In turn, the inner in_sequence() does not terminate its nested task work_b(), because at the moment when an exception occurs, inner in_sequence() is not yet running and the error occurs.

In further experiments, I “invented” the following version of in_sequence() which seems to solve the problem:

async def in_sequence(name: str, *tasks: Coroutine):
    """Executes coroutines in sequence"""
    print(f"in_sequence '{name}' start")
    tasks = iter(tasks)
    try:
        for task in tasks:
            await task

    except asyncio.CancelledError as c:
        print(f"+++ in_sequence '{name}' catch CancelledError: {c}")

    finally:
        # Terminates all other tasks scheduled for execution in event loop
        for remaining_task in tasks:
            print(f"+++ Cancel {remaining_task}")
            remaining_task.send(None)
            try:
                # Will be raised asyncio.CancelledError in coroutine
                remaining_task.throw(asyncio.CancelledError)

            except asyncio.CancelledError:
                pass
            # remaining_task.close()                                                      # >>> RuntimeWarning: coroutine 'work_b' was never awaited
            # asyncio.create_task(remaining_task).cancel()                                # >>> RuntimeWarning: coroutine 'work_b' was never awaited
            # asyncio.create_task(remaining_task).set_exception(asyncio.CancelledError)   # >>> Exception - Task does not support set_exception operation

    print(f"in_sequence '{name}' finish")

and returns the following output:

in_sequence '1' start
Work 'A' start
Work 'A' finish
raise_exception executed
+++ Cancel <coroutine object in_sequence at 0x0000024AF58B6AC0>
in_sequence '2' start
Work 'B' start
+++ in_sequence '2' catch CancelledError: 
in_sequence '2' finish
+++ Quit with exception: coroutine raised StopIteration

The questions:

  1. Is this the correct solution or are there better options? How to terminate child tasks in such a situation?
  2. Is it normal that a 'coroutine raised StopIteration' exception throws in my implementation?

来源:https://stackoverflow.com/questions/61070740/how-to-properly-terminate-nested-asyncio-tasks

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!