различать отмену экранированной задачи и текущей задачи - PullRequest
4 голосов
/ 29 марта 2019

при чтении: https://docs.python.org/3/library/asyncio-task.html#asyncio.Task.cancel похоже, что перехват CancelledError используется для двух целей.

Один потенциально препятствует отмене вашей задачи.

Другой определяет, что что-то отменило задачу, которую вы ожидаете. Как определить разницу?

async def cancel_me():
    try:
        await asyncio.sleep(3600)
    except asyncio.CancelledError:
        raise
    finally:
        print('cancel_me(): after sleep')

async def main():
    task = asyncio.create_task(cancel_me())
    await asyncio.sleep(1)
    task.cancel()
    try:
        await task
    except asyncio.CancelledError:
        # HERE: How do I know if `task` has been cancelled, or I AM being cancelled?
        print("main(): cancel_me is cancelled now")

Ответы [ 2 ]

3 голосов
/ 29 марта 2019

Как определить разницу [между тем, как нас отменяют, и задачей, которую мы ожидаем отмены]?

Асинсио не позволяет легко понять разницу. Когда внешняя задача ожидает внутренней задачи, она передает управление своей внутренней функции. В результате при отмене любой задачи CancelledError вводится в одно и то же место: самый внутренний await внутри внутренней задачи. Вот почему вы не можете сказать, какая из двух задач была отменена изначально.

Однако, можно обойти проблему, разорвав цепочку await s и связав задачи, используя вместо этого обратный вызов завершения. Отмена внутренней задачи затем перехватывается и обнаруживается в обратном вызове:

class ChildCancelled(asyncio.CancelledError):
    pass

async def detect_cancel(task):
    cont = asyncio.get_event_loop().create_future()
    def on_done(_):
        if task.cancelled():
            cont.set_exception(ChildCancelled())
        elif task.exception() is not None:
            cont.set_exception(task.exception())
        else:
            cont.set_result(task.result())
    task.add_done_callback(on_done)
    await cont

Это функционально эквивалентно await task, за исключением того, что оно не ожидает внутреннего task напрямую; он ожидает фиктивное будущее, результат которого устанавливается после завершения task. На этом этапе мы можем заменить CancelledError (который, как мы знаем, должен был прийти из отмены внутренней задачи), на более конкретный ChildCancelled. С другой стороны, если внешняя задача отменяется, она будет отображаться как обычная CancelledError в await cont и будет распространяться как обычно.

Вот тестовый код:

import asyncio, sys

# async def detect_cancel defined as above

async def cancel_me():
    print('cancel_me')
    try:
        await asyncio.sleep(3600)
    finally:
        print('cancel_me(): after sleep')

async def parent(task):
    await asyncio.sleep(.001)
    try:
        await detect_cancel(task)
    except ChildCancelled:
        print("parent(): child is cancelled now")
        raise
    except asyncio.CancelledError:
        print("parent(): I am cancelled")
        raise

async def main():
    loop = asyncio.get_event_loop()
    child_task = loop.create_task(cancel_me())
    parent_task = loop.create_task(parent(child_task))
    await asyncio.sleep(.1)  # give a chance to child to start running
    if sys.argv[1] == 'parent':
        parent_task.cancel()
    else:
        child_task.cancel()
    await asyncio.sleep(.5)

asyncio.get_event_loop().run_until_complete(main())

Обратите внимание, что в этой реализации отмена внешней задачи не приведет к автоматической отмене внутренней, но это можно легко изменить с помощью явного вызова child.cancel(), либо в parent, либо в detect_cancel.

Asyncio использует аналогичный подход к машине asyncio.shield().

0 голосов
/ 02 апреля 2019

Контекст

Во-первых, давайте рассмотрим более широкий контекст:

caller() --> your_coro() --> callee()

Вы управляете своей сопрограммой, но не контролируете вызывающих и только вчастичный контроль вызываемых абонентов.

По умолчанию отмена эффективно "распространяется" как вверх, так и вниз по стеку:

(1)
caller1() ------------------+    (2)
                            +--> callee()
caller2() --> your_coro() --+
(4)          (3)

На этой диаграмме семантически и очень свободно , если caller1() активно отменяется, то callee() отменяется, и тогда ваша сопрограмма отменяется, а затем caller2() отменяется.Примерно то же самое происходит, если caller2() активно отменяется.

(callee() является общим, и, следовательно, не простой сопрограммой, скорее Task или Future)

Что альтернативное поведение возможно, вы захотите?

Щит

Если вы хотите, чтобы callee() продолжалось, даже если caller2() отменено, shield это:

callee_f = asyncio.ensure_future(callee())

async def your_coro():
    # I might die, but I won't take callee down with me
    await asyncio.shield(callee_f)

Обратный щит

Если вы позволите callee() умереть, но хотите, чтобы ваша сопрограмма продолжалась, преобразуйте исключение :

async def reverse_shield(awaitable):
    try:
        return await awaitable
    except asyncio.CancelledError:
        raise Exception("custom")

async def your_coro():
    await reverse_shield(callee_f)
    # handle custom exception

Защитите себя

Этот вопрос сомнителен - обычно вы должны разрешить своему вызывающему абоненту отменять сопрограмму.

Примечательным исключением является случай, когда ваш вызывающий абонент является фреймворком, и он не настраивается.

def your_coro():
    async def inner():
        ...
    return asyncio.shield(inner())
...