[Python-ideas] How the heck does async/await work in Python 3.5
Terry Reedy
tjreedy at udel.edu
Mon Mar 14 15:52:25 EDT 2016
On 2/25/2016 11:47 AM, Guido van Rossum wrote:
> On Wed, Feb 24, 2016 at 10:49 PM, Terry Reedy <tjreedy at udel.edu> wrote:
>> My memory is that any replacement event loop is expected to be a full
>> implmentation of the network io loop, so one cannot just make a gui loop
>> that works with coroutines.
>
> That's not correct. If an I/O loop does not implement the network I/O
> APIs it just cannot be used for network I/O, but it can still
> implement all the other functionality (like call_later).
The tk loop already has the run and call functionality (and gui event
stuff). The reason I want to integrate tkinter and asyncio is to be
able to use the I/O APIs.
In particular, I want to experiment with subprocess_exec. Assuming that
my reading of the PEP is correct, I want to try using it run code
checkers on code (such as the contents of an IDLE editor file). A GSOC
student worked out everything but how to do so without blocking and
freezing the ui. Beyond that, we need to replace the unreliable (but
non-blocking) socket connection between IDLE and the user execution
process).
> There are
> other loops that don't implement everything (e.g. the subprocess and
> datagram APIs are not supported by Proactor,
According to the PEP and doc
https://docs.python.org/3/library/asyncio-eventloops.html#windows,
the Proactor is the loop that supports subprocesses on Windows, while
Windows Selector does not. I will find out for sure soon enough.
> and signal support is
> spotty, too). If you find some implementation barrier that prevents
> that from working it should be fixed.
After lots of reading and thinking, I decided to start with simplest
this that might work, and came up with the following surprisingly simple
subclass. It works with GUI versions of the first two examples in the
doc. It will satisfy many simple cases of using tkinter and asyncio
together.
I will continue with other examples The docstring describes possible
ways to improve the integration.
I am not sure whether root.update handles the events handled by
update_idletasks (or where 'idletasks' come from). If not,
update_idletask() calls may be needed. Serhiy should know.
---
'''Proof of concept for integrating asyncio and tk loops.
Terry Jan Reedy
Run with 'python -i' or from IDLE editor to keep tk window alive.
This version simply calls tk root.update in run_forever.
Without this, the tk window only appears after loop.stop,
unless root.update() is put in callbacks instead.
If there are no pending asyncio callbacks, self._run_once
will block indefinitely in a .select(None) call, waiting
for an I/O event. To ensure that all tk events and callbacks
get timely attention, either _run_once should be revised or
__init__ should start an asyncio callback loop so that there is always a
pending callback. The details will depend on whether the GUI or network
IO is intended to get priority attention.
'''
import asyncio as ai
import threading
import tkinter as tk
import datetime # for example
# On Windows, need proactor for subprocess functions
EventLoop = ai.ProactorEventLoop
# Following gets ai.SelectorEventLoop
# EventLoop = type(ai.get_event_loop())
class TkEventLoop(EventLoop):
def __init__(self, root):
self.root = root
super().__init__()
def run_forever(self): # copy with 1 new line for proof of concept
"""Run until stop() is called."""
self._check_closed()
if self.is_running():
raise RuntimeError('Event loop is running.')
self._set_coroutine_wrapper(self._debug)
self._thread_id = threading.get_ident()
try:
while True:
self._run_once() # can block indefinitely
self.root.update() # new line
if self._stopping:
break
finally:
self._stopping = False
self._thread_id = None
self._set_coroutine_wrapper(False)
root = tk.Tk()
loop = TkEventLoop(root)
ai.set_event_loop(loop)
# Combine 2 event loop examples from BaseEventLoop doc.
hello = tk.Label(root)
timel = tk.Label(root)
hello.pack()
timel.pack()
def hello_world(loop):
hello['text'] = 'Hello World'
loop.call_soon(hello_world, loop)
def display_date(end_time, loop):
timel['text'] = datetime.datetime.now()
if (loop.time() + 1.0) < end_time:
loop.call_later(1, display_date, end_time, loop)
else:
loop.stop()
end_time = loop.time() + 5.1
loop.call_soon(display_date, end_time, loop)
# Blocking call interrupted by loop.stop()
loop.run_forever()
---
> The await expression (or yield from) does not require an event loop.
> Heck, it doesn't require futures.
Without either, are async/await actually useful? The binary/abinary
examples just shows that there is not much penalty for using async/await
when not needed. On my machine, abinary is pretty consistently 2% slower.
> It uses coroutines and it is totally
> up to the manager of those how they are scheduled (read Greg Ewing's
> very early explanations of yield from -- some have no I/O at all).
What do you mean by 'manager of [coroutines]'? For loop, event loop,
task, the nebulous 'scheduler', or something else?
> But there's something else that you might be after.
After getting non-blocking gui and io events (with callbacks) working
together, I an after asynchronous iteration. The asyncio modules used
cooroutines for suspension, but as far as I can tell, not for iteration.
Normal synchronous iteration is fine for compute-bound iterators and a
single iterator whose delays are tolerable because the thread or process
has nothing better to do. (And the system will switch to something else
when the thread blocks, so the cpu is not actually blocked.) But it
does not work for concurrent intermitant streams.
For example, consider the datetime display in the example above. The
code is scattered around in three places. I would like to be able to
replace the pieces with synchronous-like code, something like the following.
async def display_date(interval, end_time, loop=ai.get_event_loop()):
label = tk.Label(root)
label.pack()
async for tick in timer_soon(interval, end_time, loop):
label['text'] = datetime.datetime.now()
The asynchronous iterator timer_soon would incorporate call_soon,
call_later, and stop calls and 'yield' True at the appointed times.
Doing so allows the widget creation and repeated widget event handling
to be combined in one function.
How do I write it? The async iterator examples in PEP 492 (the second
is Example 1) shows all the boiler plate, which I could write from the
spec given, but omit the essential detail of how to write the inner
cooroutine that gets awaited on.
async def fetch_data(self):
...
async def _prefetch(self):
...
???
> When you're implementing this API on top of tkinter, you'll probably
> find that you'll have to use tkinter's way of sleeping anyways, so the
> implementation of waiting in BaseEventLoop using a selector is not
> useful for this scenario.
Are you referring to the possibly indefinite blocking call to select in
._run_once? A possible rewrite is to block on a tk root.mainloop call
and make periodic non-blocking select(0) calls in callbacks. If this
were done, it might make sense to re-write call_later and call_at to use
tk's root.after and callback scheduler. I don't know if the same can be
done with call_soon while maintaining the detailed run_forever spec in
the 3.5.1 doc.
> There are probably some possible refactorings in the asyncio package
> to help you reuse a little more code,
I decided to start by reusing all code and just add a tk call.
TkEventLoop will pass the same test suite as its superclass.
> but all in all I still think it would be very useful to have an
> asyncio loop integrated with Tkinter.
Most definitely. 'An' example is trivial, as posted above.
I will see how far it goes in running other examples.
> (Of course Tkinter does support network I/O,
Tcl's createfilehandler function is only available on unix. I read
somewhere that is uses selector polling.
> or somehow figure out how to wait using a Selector
> *or* tkinter events in the same loop.
I suspect that waiting on one and polling the other is the only
cross-platform solution.
--
Terry Jan Reedy
More information about the Python-ideas
mailing list