Quantcast
Channel: Planet Python
Viewing all articles
Browse latest Browse all 22462

Nick Coghlan: Background tasks in Python 3.5

$
0
0

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.


Viewing all articles
Browse latest Browse all 22462

Trending Articles