asyncio: Why does cancelling a task lead to cancellation of other tasks added into the event loop?

Question:

I use a coroutine to add another coroutine to the event loop multiple times but partway through I cancel the first coroutine. I thought this would mean that any coroutines already added to the event loop would complete successfully and no more would be added, however I find that coroutines that have already been added to the event loop also seem to be cancelled.

I’m running this script in Spyder so I don’t need to call run_until_complete, etc. because the event loop is already running in the background on my environment.

I’m sure I’m missing something and the code is behaving exactly as it should – but I can’t figure out why. I would also like to know how I might allow cancellation of runTimes but still let slowPrinter complete.

Thank you!

Code below

import asyncio

loop = asyncio.get_event_loop()

async def runTimes(async_func, times):
    for i in range(0, times):
        task = loop.create_task(async_func())
        await task
        
async def slowPrinter():
    await asyncio.sleep(2)
    print("slowPrinter done")
    

async def doStuff():
    for i in range(0, 10):
        await(asyncio.sleep(1))
    print("doStuff done")
        
async def doLater(delay_ms, method, *args, **kwargs):
    try:
        print("doLater " + str(delay_ms) + " " + str(method.__name__))
    except AttributeError:
        print("doLater " + str(delay_ms))
    await asyncio.sleep(delay_ms/1000)
    method(*args, **kwargs)
    print("doLater complete")
        
task = loop.create_task(runTimes(slowPrinter, 3))
loop.create_task(doLater(3000, task.cancel))
loop.create_task(doStuff())


Output

doLater 3000 cancel
slowPrinter done
doLater complete
doStuff done

Expected Output

doLater 3000 cancel
slowPrinter done
doLater complete
**slowPrinter done**
doStuff done

Edit: Part of the reason I have built the code without using things like run_later is because I need to port the code to micropython later so I am sticking to functions I can use on micropython.

Edit2: Interestingly, task cancellation seems to propagate to tasks created from within the coroutine as well!

async def runTimes(async_func, times):
    for i in range(0, times):
        task = loop.create_task(async_func())
        try:
            await task
        except asyncio.CancelledError:
            print("cancelled as well")

Output

doLater 3000 cancel
slowPrinter done
doLater complete
cancelled as well
slowPrinter done
doStuff done
Asked By: Vitalize0769

||

Answers:

That’s because your task is waiting on another task:

async def runTimes(async_func, times):
    for i in range(0, times):
        task = loop.create_task(async_func())
        await task ## HERE!

As per asyncio’s documentation:

To cancel a running Task use the cancel() method. Calling it will
cause the Task to throw a CancelledError exception into the wrapped
coroutine. If a coroutine is awaiting on a Future object during
cancellation, the Future object will be cancelled.

You may have to look for a way to prevent the task from being cancelled while waiting.

Answered By: ichramm

With the second edit where I used this code it became clear the await expression was where the cancellation error was being thrown.

async def runTimes(async_func, times):
    for i in range(0, times):
        task = loop.create_task(async_func())
        try:
            await task
        except asyncio.CancelledError:
            print("cancelled as well")

Changing to awaiting on a timer allowed the main task to complete and the cancellation was thrown on the await asyncio.sleep() method instead which works for my use case.

import asyncio

loop = asyncio.get_event_loop()


async def runTimes(async_func, times):
    global current_task
    for i in range(0, times):
        current_task = loop.create_task(async_func())
        while not current_task.done():
            await asyncio.sleep(0.05)
        
async def slowPrinter():
    await asyncio.sleep(2)
    print("slowPrinter done")
    

async def doStuff():
    for i in range(0, 10):
        await(asyncio.sleep(1))
    print("doStuff done")
        
async def doLater(delay_ms, method, *args, **kwargs):
    try:
        print("doLater " + str(delay_ms) + " " + str(method.__name__))
    except AttributeError:
        print("doLater " + str(delay_ms))
    await asyncio.sleep(delay_ms/1000)
    method(*args, **kwargs)
    print("doLater complete")
    
        
task = loop.create_task(runTimes(slowPrinter, 3))
loop.create_task(doLater(3000, task.cancel))
loop.create_task(doStuff())

Answered By: Vitalize0769
Categories: questions Tags: ,
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.