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
.
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()
# ...