web-dev-qa-db-fra.com

Veuillez expliquer "La tâche a été détruite mais elle est en attente!"

Python 3.4.2

J'apprends asyncio et je l'utilise pour écouter continuellement IPC bus, pendant que gbulb écoute le dbus.

Quelques notes annexes:

J'ai donc créé une fonction listen_to_ipc_channel_layer Qui écoute continuellement les messages entrants sur le canal IPC et transmet le message à un message_handler.

J'écoute également SIGTERM et SIGINT. Ainsi, lorsque j'envoie un SIGTERM au processus python exécutant le code que vous trouvez en bas, le script doit se terminer avec élégance.

Le problème

… Je reçois l'avertissement suivant:

got signal 15: exit
Task was destroyed but it is pending!
task: <Task pending coro=<listen_to_ipc_channel_layer() running at /opt/mainloop-test.py:23> wait_for=<Future cancelled>>

Process finished with exit code 0

… Avec le code suivant:

import asyncio
import gbulb
import signal
import asgi_ipc as asgi

def main():
    asyncio.async(listen_to_ipc_channel_layer())
    loop = asyncio.get_event_loop()

    for sig in (signal.SIGINT, signal.SIGTERM):
        loop.add_signal_handler(sig, ask_exit)

    # Start listening on the Linux IPC bus for incoming messages
    loop.run_forever()
    loop.close()

@asyncio.coroutine
def listen_to_ipc_channel_layer():
    """Listens to the Linux IPC bus for messages"""
    while True:
        message_handler(message=channel_layer.receive(["my_channel"]))
        try:
            yield from asyncio.sleep(0.1)
        except asyncio.CancelledError:
            break

def ask_exit():
    loop = asyncio.get_event_loop()
    for task in asyncio.Task.all_tasks():
        task.cancel()
    loop.stop()


if __name__ == "__main__":
    gbulb.install()
    # Connect to the IPC bus
    channel_layer = asgi.IPCChannelLayer(prefix="my_channel")
    main()

Je ne comprends toujours que très peu d'asyncio, mais je pense que je sais ce qui se passe. En attendant yield from asyncio.sleep(0.1), le gestionnaire de signal a intercepté le SIGTERM et dans ce processus, il appelle task.cancel().

Question posée: cela ne devrait-il pas déclencher le CancelledError dans la boucle while True:? (Parce que ce n'est pas le cas, mais c'est ainsi que je comprends "L'appel de cancel () lancera une CancelledError à la coroutine encapsulée" ).

Finalement, loop.stop() est appelée, ce qui arrête la boucle sans attendre que yield from asyncio.sleep(0.1) renvoie un résultat ou même la coroutine entière listen_to_ipc_channel_layer.

S'il vous plait corrigez moi si je me trompe.

Je pense que la seule chose que je dois faire est de faire attendre à mon programme que la fonction yield from asyncio.sleep(0.1) renvoie un résultat et/ou coroutine pour sortir la boucle while et terminer .

Je crois que je confond beaucoup de choses. S'il vous plaît, aidez-moi à clarifier ces choses afin que je puisse comprendre comment fermer gracieusement la boucle d'événements sans avertissement.

20
Daniel

Le problème vient de la fermeture de la boucle immédiatement après l'annulation des tâches. Comme l'état cancel () docs

"Cela permet de renvoyer une CancelledError dans la coroutine encapsulée le cycle suivant à travers la boucle d'événement."

Prenez cet extrait de 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()

Remarque ask_exit Annule les tâches mais ne stop la boucle, au cycle suivant looping_coro() l'arrête. La sortie si vous l'annulez est:

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

Remarquez comment pending_Doom Annule et arrête la boucle immédiatement après. Si vous le laissez fonctionner jusqu'à ce que les coroutines pending_Doom Se réveillent du sommeil, vous pouvez voir le même avertissement que vous recevez:

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>>
15
Yeray Diaz Diaz

La signification du problème est qu'une boucle n'a pas le temps de terminer toutes les tâches.

Cela permet de renvoyer une CancelledError dans la coroutine encapsulée lors du cycle suivant de la boucle d'événements.

Il n'y a aucune chance de faire un "cycle suivant" de la boucle dans votre approche. Pour le faire correctement, vous devez déplacer une opération d'arrêt vers une coroutine non cyclique distincte pour donner à votre boucle une chance de terminer.

La deuxième chose importante est CancelledError augmentation.

Contrairement à Future.cancel (), cela ne garantit pas que la tâche sera annulée: l'exception pourrait être interceptée et exécutée, retardant l'annulation de la tâche ou empêchant complètement l'annulation. La tâche peut également renvoyer une valeur ou déclencher une exception différente.

Immédiatement après l'appel de cette méthode, cancelled () ne renverra pas True (sauf si la tâche a déjà été annulée). Une tâche sera marquée comme annulée lorsque la coroutine encapsulée se termine avec une exception CancelledError (même si cancel () n'a pas été appelée).

Ainsi, après le nettoyage, votre coroutine doit augmenter CancelledError pour être marquée comme annulée.

L'utilisation d'une coroutine supplémentaire pour arrêter la boucle n'est pas un problème car elle n'est pas cyclique et doit être effectuée immédiatement après l'exécution.

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()                               
7
I159