web-dev-qa-db-fra.com

L'utilisation de files d'attente entraîne une exception asyncio "a obtenu Future <Future en attente> attaché à une boucle différente"

J'essaie d'exécuter ce code simple avec des files d'attente asynchrones, mais intercepter des exceptions, et même des exceptions imbriquées.

Je voudrais obtenir de l'aide pour faire fonctionner correctement les files d'attente dans asyncio:

import asyncio, logging

logging.basicConfig(level=logging.DEBUG)
logging.getLogger("asyncio").setLevel(logging.WARNING)


num_workers = 1
in_queue = asyncio.Queue()
out_queue = asyncio.Queue()
tasks = []


async def run():
    for request in range(1):
        await in_queue.put(request)

    # each task consumes from 'input_queue' and produces to 'output_queue':
    for i in range(num_workers):
        tasks.append(asyncio.create_task(worker(name=f'worker-{i}')))
    # tasks.append(asyncio.create_task(saver()))

    print('waiting for queues...')
    await in_queue.join()
    # await out_queue.join()
    print('all queues done')

    for task in tasks:
        task.cancel()
    print('waiting until all tasks cancelled')
    await asyncio.gather(*tasks, return_exceptions=True)
    print('done')


async def worker(name):
    while True:
        try:
            print(f"{name} started")
            num = await in_queue.get()
            print(f'{name} got {num}')
            await asyncio.sleep(0)
            # await out_queue.put(num)
        except Exception as e:
            print(f"{name} exception {e}")
        finally:
            print(f"{name} ended")
            in_queue.task_done()


async def saver():
    while True:
        try:
            print("saver started")
            num = await out_queue.get()
            print(f'saver got {num}')
            await asyncio.sleep(0)
            print("saver ended")
        except Exception as e:
            print(f"saver exception {e}")
        finally:
            out_queue.task_done()


asyncio.run(run(), debug=True)
print('Done!')

Production:

waiting for queues...
worker-0 started
worker-0 got 0
worker-0 ended
worker-0 started
worker-0 exception 
worker-0 ended
ERROR:asyncio:unhandled exception during asyncio.run() shutdown
task: <Task finished coro=<worker() done, defined at temp4.py:34> exception=ValueError('task_done() called too many times') created at Python37\lib\asyncio\tasks.py:325>
Traceback (most recent call last):
  File "Python37\lib\asyncio\runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "Python37\lib\asyncio\base_events.py", line 573, in run_until_complete
    return future.result()
  File "temp4.py", line 23, in run
    await in_queue.join()
  File "Python37\lib\asyncio\queues.py", line 216, in join
    await self._finished.wait()
  File "Python37\lib\asyncio\locks.py", line 293, in wait
    await fut
RuntimeError: Task <Task pending coro=<run() running at temp4.py:23> cb=[_run_until_complete_cb() at Python37\lib\asyncio\base_events.py:158] created at Python37\lib\asyncio\base_events.py:552> got Future <Future pending> attached to a different loop

During handling of the above exception, another exception occurred:

Traceback (most recent call last):
  File "temp4.py", line 46, in worker
    in_queue.task_done()
  File "Python37\lib\asyncio\queues.py", line 202, in task_done
    raise ValueError('task_done() called too many times')
ValueError: task_done() called too many times
Traceback (most recent call last):
  File "C:\Program Files\JetBrains\PyCharm Community Edition 2018.1.4\helpers\pydev\pydevd.py", line 1664, in <module>
    main()
  File "C:\Program Files\JetBrains\PyCharm Community Edition 2018.1.4\helpers\pydev\pydevd.py", line 1658, in main
    globals = debugger.run(setup['file'], None, None, is_module)
  File "C:\Program Files\JetBrains\PyCharm Community Edition 2018.1.4\helpers\pydev\pydevd.py", line 1068, in run
    pydev_imports.execfile(file, globals, locals)  # execute the script
  File "C:\Program Files\JetBrains\PyCharm Community Edition 2018.1.4\helpers\pydev\_pydev_imps\_pydev_execfile.py", line 18, in execfile
    exec(compile(contents+"\n", file, 'exec'), glob, loc)
  File "temp4.py", line 63, in <module>
    asyncio.run(run(), debug=True)
  File "Python37\lib\asyncio\runners.py", line 43, in run
    return loop.run_until_complete(main)
  File "Python37\lib\asyncio\base_events.py", line 573, in run_until_complete
    return future.result()
  File "temp4.py", line 23, in run
    await in_queue.join()
  File "Python37\lib\asyncio\queues.py", line 216, in join
    await self._finished.wait()
  File "Python37\lib\asyncio\locks.py", line 293, in wait
    await fut
RuntimeError: Task <Task pending coro=<run() running at temp4.py:23> cb=[_run_until_complete_cb() at Python37\lib\asyncio\base_events.py:158] created at Python37\lib\asyncio\base_events.py:552> got Future <Future pending> attached to a different loop

Ceci est le flux de base, ce que je voudrais faire plus tard, c'est exécuter plus de demandes sur plus de travailleurs où chaque travailleur déplacera le nombre de in_queue à out_queue puis l'économiseur imprime les chiffres de out_queue.

9
Shirkan

Vos files d'attente doivent être créées à l'intérieur de la boucle . Vous les avez créés en dehors de la boucle créée pour asyncio.run(), ils utilisent donc events.get_event_loop(). asyncio.run() crée une nouvelle boucle et les futurs créés pour la file d'attente dans une boucle ne peuvent pas être utilisés dans l'autre.

Créez vos files d'attente dans votre coroutine de niveau supérieur run() et transmettez-les aux coroutines qui en ont besoin ou utilisez contextvars.ContextVar objets si vous devez utiliser des globaux.

Vous devez également nettoyer la façon dont vous gérez l'annulation des tâches dans vos tâches. Une tâche est annulée en levant une asyncio.CancelledError exception dans la tâche . Vous pouvez l'ignorer, mais si vous l'attrapez pour faire un travail de nettoyage, vous devez le surélever.

Votre code de tâche intercepte toutes les exceptions sans relancer, y compris CancelledError, afin de bloquer les annulations appropriées.

Au lieu de cela, ce qui se produit pendant l'annulation est que vous appelez queue.task_done() ; ne faites pas cela, du moins pas lorsque votre tâche est annulée. Vous ne devez appeler task_done() que lorsque vous gérez réellement une tâche de file d'attente, mais votre code appelle task_done() lorsqu'une exception se produit en attendant qu'une tâche de file d'attente apparaisse .

Si vous devez utiliser try...finally: in_queue.task_done(), placez-le autour du bloc de code qui gère un élément reçu de la file d'attente et conservez le await in_queue.get() à l'extérieur de ce bloc try. Vous ne voulez pas marquer les tâches terminées que vous n'avez pas réellement reçues.

Enfin, lorsque vous imprimez des exceptions, vous souhaitez imprimer leur repr(); pour des raisons historiques, la conversion str() des exceptions produit leur valeur .args, ce qui n'est pas très utile pour les exceptions CancelledError, qui ont un .args vide . Utilisez {e!r} dans les chaînes formatées, afin que vous puissiez voir quelle exception vous interceptez:

worker-0 exception CancelledError()

Ainsi, le code corrigé, avec la tâche saver() activée, les files d'attente créées à l'intérieur de run() et la gestion des exceptions de tâche nettoyées, serait:

import asyncio, logging

logging.basicConfig(level=logging.DEBUG)
logging.getLogger("asyncio").setLevel(logging.WARNING)


num_workers = 1


async def run():
    in_queue = asyncio.Queue()
    out_queue = asyncio.Queue()

    for request in range(1):
        await in_queue.put(request)

    # each task consumes from 'in_queue' and produces to 'out_queue':
    tasks = []
    for i in range(num_workers):
        tasks.append(asyncio.create_task(
            worker(in_queue, out_queue, name=f'worker-{i}')))
    tasks.append(asyncio.create_task(saver(out_queue)))

    await in_queue.join()
    await out_queue.join()

    for task in tasks:
        task.cancel()

    await asyncio.gather(*tasks, return_exceptions=True)

    print('done')

async def worker(in_queue, out_queue, name):
    print(f"{name} started")
    try:
        while True:
            num = await in_queue.get()
            try:
                print(f'{name} got {num}')
                await asyncio.sleep(0)
                await out_queue.put(num)
            except Exception as e:
                print(f"{name} exception {e!r}")
                raise
            finally:
                in_queue.task_done()
    except asyncio.CancelledError:
        print(f"{name} is being cancelled")
        raise
    finally:
        print(f"{name} ended")

async def saver(out_queue):
    print("saver started")
    try:
        while True:
            num = await out_queue.get()
            try:
                print(f'saver got {num}')
                await asyncio.sleep(0)
                print("saver ended")
            except Exception as e:
                print(f"saver exception {e!r}")
                raise
            finally:
                out_queue.task_done()
    except asyncio.CancelledError:
        print(f"saver is being cancelled")
        raise
    finally:
        print(f"saver ended")

asyncio.run(run(), debug=True)
print('Done!')

Cela imprime

worker-0 started
worker-0 got 0
saver started
saver got 0
saver ended
done
worker-0 is being cancelled
worker-0 ended
saver is being cancelled
saver ended
Done!

Si vous souhaitez utiliser des globaux, pour partager des objets de file d'attente, utilisez des objets ContextVar. Vous créez toujours les files d'attente dans run(), mais si vous deviez démarrer plusieurs boucles, l'intégration du module contextvars se chargera de garder les files d'attente séparées:

from contextvars import ContextVar
# ...

in_queue = ContextVar('in_queue')
out_queue = ContextVar('out_queue')

async def run():
    in_, out = asyncio.Queue(), asyncio.Queue()
    in_queue.set(in_)
    out_queue.set(out)

    for request in range(1):
        await in_.put(request)

    # ...

    for i in range(num_workers):
        tasks.append(asyncio.create_task(worker(name=f'worker-{i}')))
    tasks.append(asyncio.create_task(saver()))

    await in_.join()
    await out.join()

    # ...

async def worker(name):
    print(f"{name} started")
    in_ = in_queue.get()
    out = out_queue.get()
    try:
        while True:
            num = await in_.get()
            try:
                # ...
                await out.put(num)
                # ...
            finally:
                in_.task_done()
    # ...

async def saver():
    print("saver started")
    out = out_queue.get()
    try:
        while True:
            num = await out.get()
            try:
                # ...
            finally:
                out.task_done()
    # ...
24
Martijn Pieters