One of the recurring questions with asyncio is "How do I execute one or two operations asynchronously in an otherwise synchronous application?"
Say, for example, I have the following code:
>>>importitertools,time>>>defticker():...foriinitertools.count():...print(i)...time.sleep(1)...>>>ticker()0123^CTraceback(mostrecentcalllast):File"<stdin>",line1,in<module>File"<stdin>",line4,intickerKeyboardInterrupt
With the native coroutine syntax coming in Python 3.5, I can change that synchronous code into event-driven asynchronous code easily enough:
asyncdefticker():foriinitertools.count():print(i)awaitasyncio.sleep(1)
But how do I kick that ticker off as a background task? What's the Python
REPL's equivalent of appending &
to a shell command?
It turns out it looks something like this:
importasynciodefrun_in_background(target,*,loop=None):"""Schedules target as a background taskReturnsthescheduledtask.Iftargetisafutureorcoroutine,equivalenttoasyncio.ensure_futureIftargetisacallable,itisscheduledinthedefaultexecutor"""ifloopisNone:loop=asyncio.get_event_loop()try:returnasyncio.ensure_future(target,loop=loop)exceptTypeError:passifcallable(target):returnloop.run_in_executor(None,target)raiseTypeError("background task must be future, coroutine or ""callable, not {!r}".format(type(target)))
So now I can do:
>>>asyncdefticker():...foriinitertools.count():...print(i)...awaitasyncio.sleep(1)...>>>ticker1=run_in_background(ticker())>>>ticker1<Taskpendingcoro=<ticker()runningat<stdin>:1>>
But how do I run that for a while? The event loop won't run unless the current thread starts it running and either stops when a particular event occurs, or when explicitly stopped. Another helper function covers that:
defrun_in_foreground(task,*,loop=None):"""Runs event loop in current thread until the given task completesReturnstheresultofthetask.Formorecomplexconditions,combinewithasyncio.wait()Toincludeatimeout,combinewithasyncio.wait_for()"""ifloopisNone:loop=asyncio.get_event_loop()returnloop.run_until_complete(asyncio.ensure_future(task))
And then I can do:
>>>run_in_foreground(asyncio.sleep(5))01234
Here we can see the background task running while we wait for the foreground task to complete. And if I do it again with a different timeout:
>>>run_in_foreground(asyncio.sleep(3))567
We see that the background task picked up again right where it left off the first time.
We can also single step the event loop with a zero second sleep (the ticks reflect the fact there was more than a second delay between running each command):
>>>run_in_foreground(asyncio.sleep(0))8>>>run_in_foreground(asyncio.sleep(0))9
And start a second ticker to run concurrently with the first one:
>>>ticker2=run_in_background(ticker())>>>ticker2<Taskpendingcoro=<ticker()runningat<stdin>:1>>>>>run_in_foreground(asyncio.sleep(0))010
The asynchronous tickers will happily hang around in the background, ready to resume operation whenever I give them the opportunity. If I decide I want to stop one of them, I can cancel the corresponding task:
>>>ticker1.cancel()True>>>run_in_foreground(asyncio.sleep(0))1>>>ticker2.cancel()True>>>run_in_foreground(asyncio.sleep(0))
But what about our original synchronous ticker? Can I run that as a
background task? It turns out I can, as that's the reason for the special
handling of callables in run_in_background
. However, I haven't figured out
how to reliably cancel a task created through run_in_executor
so we'll make
sure this variant of the synchronous version stops on its own:
importitertools,timedefticker_sync(stop):foriinrange(stop):print(i)time.sleep(1)print("Finishing")
The key difference between scheduling a callable and a coroutine, is that the callable will start executing immediately in another thread, rather than waiting for the current thread to run the event loop:
>>>threaded_ticker=run_in_background(lambda:ticker_sync(5));print("Starts immediately!")0Startsimmediately!>>>1234Finishing
That's both a strength (as you can run multiple blocking IO operations in parallel), but also a significant weakness - one of the benefits of explicit coroutines is their predictability, as you know none of them will start doing anything until you start running the event loop.