Please explain “Task was destroyed but it is pending!”

前端 未结 4 761
难免孤独
难免孤独 2021-02-06 22:54

Python 3.4.2

I am learning asyncio and I use it to continously listen IPC bus, while gbulb listens to the dbus.

Some side notes:

So I created a fun

相关标签:
4条回答
  • 2021-02-06 22:55

    The meaning of the issue is that a loop doesn't have time to finish all the tasks.

    This arranges for a CancelledError to be thrown into the wrapped coroutine on the next cycle through the event loop.

    There is no chance to do a "next cycle" of the loop in your approach. To make it properly you should move a stop operation to a separate non-cyclic coroutine to give your loop a chance to finish.

    Second significant thing is CancelledError raising.

    Unlike Future.cancel(), this does not guarantee that the task will be cancelled: the exception might be caught and acted upon, delaying cancellation of the task or preventing cancellation completely. The task may also return a value or raise a different exception.

    Immediately after this method is called, cancelled() will not return True (unless the task was already cancelled). A task will be marked as cancelled when the wrapped coroutine terminates with a CancelledError exception (even if cancel() was not called).

    So after cleanup your coroutine must raise CancelledError to be marked as cancelled.

    Using an extra coroutine to stop the loop is not an issue because it is not cyclic and be done immediately after execution.

    def main():                                              
        loop = asyncio.get_event_loop()                      
        asyncio.ensure_future(listen_to_ipc_channel_layer()) 
                                                         
        for sig in (signal.SIGINT, signal.SIGTERM):          
            loop.add_signal_handler(sig, ask_exit)           
        loop.run_forever()                                   
        print("Close")                                       
        loop.close()                                         
                                                         
                                                         
    @asyncio.coroutine                                       
    def listen_to_ipc_channel_layer():                       
        while True:                                          
            try:                                             
                print("Running")                                 
                yield from asyncio.sleep(0.1)                
            except asyncio.CancelledError as e:              
                print("Break it out")                        
                raise e # Raise a proper error
                                                         
                                              
    # Stop the loop concurrently           
    @asyncio.coroutine                                       
    def exit():                                              
        loop = asyncio.get_event_loop()                      
        print("Stop")                                        
        loop.stop()                                          
    
    
    def ask_exit():                          
        for task in asyncio.Task.all_tasks():
            task.cancel()                    
        asyncio.ensure_future(exit())        
                                         
                                         
    if __name__ == "__main__":               
        main()                               
    
    0 讨论(0)
  • 2021-02-06 23:00

    The problem comes from closing the loop immediately after cancelling the tasks. As the cancel() docs state

    "This arranges for a CancelledError to be thrown into the wrapped coroutine on the next cycle through the event loop."

    Take this snippet of code:

    import asyncio
    import signal
    
    
    async def pending_doom():
        await asyncio.sleep(2)
        print(">> Cancelling tasks now")
        for task in asyncio.Task.all_tasks():
            task.cancel()
    
        print(">> Done cancelling tasks")
        asyncio.get_event_loop().stop()
    
    
    def ask_exit():
        for task in asyncio.Task.all_tasks():
            task.cancel()
    
    
    async def looping_coro():
        print("Executing coroutine")
        while True:
            try:
                await asyncio.sleep(0.25)
            except asyncio.CancelledError:
                print("Got CancelledError")
                break
    
            print("Done waiting")
    
        print("Done executing coroutine")
        asyncio.get_event_loop().stop()
    
    
    def main():
        asyncio.async(pending_doom())
        asyncio.async(looping_coro())
    
        loop = asyncio.get_event_loop()
        for sig in (signal.SIGINT, signal.SIGTERM):
            loop.add_signal_handler(sig, ask_exit)
    
        loop.run_forever()
    
        # I had to manually remove the handlers to
        # avoid an exception on BaseEventLoop.__del__
        for sig in (signal.SIGINT, signal.SIGTERM):
            loop.remove_signal_handler(sig)
    
    
    if __name__ == '__main__':
        main()
    

    Notice ask_exit cancels the tasks but does not stop the loop, on the next cycle looping_coro() stops it. The output if you cancel it is:

    Executing coroutine
    Done waiting
    Done waiting
    Done waiting
    Done waiting
    ^CGot CancelledError
    Done executing coroutine
    

    Notice how pending_doom cancels and stops the loop immediately after. If you let it run until the pending_doom coroutines awakes from the sleep you can see the same warning you're getting:

    Executing coroutine
    Done waiting
    Done waiting
    Done waiting
    Done waiting
    Done waiting
    Done waiting
    Done waiting
    >> Cancelling tasks now
    >> Done cancelling tasks
    Task was destroyed but it is pending!
    task: <Task pending coro=<looping_coro() running at canceling_coroutines.py:24> wait_for=<Future cancelled>>
    
    0 讨论(0)
  • 2021-02-06 23:08

    The reasons this happens is as explained by @Yeray Diaz Diaz In my case, I wanted to cancel all the tasks that were not done after the first finished, so I ended up cancelling the extra jobs, then using loop._run_once() to run the loop a bit more and allow them to stop:

        loop = asyncio.get_event_loop()
        job = asyncio.wait(tasks, return_when=asyncio.FIRST_COMPLETED)
        tasks_finished,tasks_pending, = loop.run_until_complete(job)
        tasks_done = [t for t in tasks_finished if t.exception() is None]
        if tasks_done == 0:
            raise Exception("Failed for all tasks.")
        assert len(tasks_done) == 1
        data = tasks_done[0].result()
        for t in tasks_pending:
            t.cancel()
            t.cancel()
        while not all([t.done() for t in tasks_pending]):
            loop._run_once()
    
    0 讨论(0)
  • 2021-02-06 23:15

    I had this message and I believe it was caused by garbage collection of pending task. The Python developers were debating whether tasks created in asyncio should create strong references and decided they shouldn't (after 2 days of looking into this problem I strongly disagree! ... see the discussion here https://bugs.python.org/issue21163)

    I created this utility for myself to make strong references to tasks and automatically clean it up (still need to test it thoroughly)...

    import asyncio
    
    #create a strong reference to tasks since asyncio doesn't do this for you
    task_references = set()
    
    def register_ensure_future(coro):
        task = asyncio.ensure_future(coro)
        task_references.add(task)
    
        # Setup cleanup of strong reference on task completion...
        def _on_completion(f):
            task_references.remove(f)
        task.add_done_callback(_on_completion)
        
        return task
    

    It seems to me that tasks should have a strong reference for as long as they are active! But asyncio doesn't do that for you so you can have some bad surprises once gc happens and long hours of debugging.

    0 讨论(0)
提交回复
热议问题