10

I'm working on a simple graphical network application, using asyncio and tkinter. I'm running into the problem of combining the asyncio event loop with Tk's mainloop. If possible, I'd like to do it without threads, because both these libraries (but especially tkinter) aren't very thread safe. Currently, I'm using Tk.update in an asyncio coroutine, which runs only a single iteration of the tk event loop:

@asyncio.coroutine
def run_tk(tk, interval=0.1):
    try:
        while True:
            tk.update()
            yield from asyncio.sleep(interval)
    except TclError as e:
        if "application has been destroyed" not in e.args[0]:
            raise

However, in the interest of exploring all options, I was wondering if it was possible to do the reverse- if it was possible to invoke only a single iteration of an asyncio event loop inside a tk callback.

Lucretiel
  • 3,145
  • 1
  • 24
  • 52
  • You can [combine the Tk main loop with asyncio](https://github.com/python/asyncio/issues/21), but I don't know if that still allows you to process network traffic (i.e. how do network events flow into the Tk main loop)? – Christian Aichinger Apr 21 '15 at 20:50
  • 1
    It appears that it would work, but it cheats- It creates a TkEventLoop, which essentially runs `update` in a loop, but also creates a traditional event loop to do network i/o and runs it in a thread. It's the worst of both worlds; the only advantage is that direct callbacks (`loop.call_later`) are invoked directly in the Tk event loop. – Lucretiel Apr 21 '15 at 21:21
  • I am missing a minimal working example in the question and in the answers. – buhtz Dec 19 '17 at 22:04
  • 1
    The accepted answer shows how to run a single step of the event loop. You should be able to repeatedly call `run_once` in the tkinter main loop to "run" asyncio. – Lucretiel Jan 23 '18 at 04:12
  • There is `loop._run_once()`. However if you stick thinter to its own thread while running asyncio loop on another there is no problem with that. Most of asyncio is not thread safe either. You can schedule coroutines and callbacks from tkinter thread with `loop.run_coroutine_threadsafe()` and `loop.call_soon_threadsafe()` respectively. – Emsi Jan 14 '20 at 17:14
  • I ended up doing this: https://github.com/Lucretiel/tkinter-async/blob/master/tkinter_async.py – Lucretiel Jan 15 '20 at 04:17

3 Answers3

12

The missing of public method like loop.run_once() is intentional. Not every supported event loop has a method to iterate one step. Often underlying API has methods for creating event loop and running it forever but emulating single step may be very ineffective.

If you really need it you may implement single-step iteration easy:

import asyncio


def run_once(loop):
    loop.call_soon(loop.stop)
    loop.run_forever()


loop = asyncio.get_event_loop()

for i in range(100):
    print('Iteration', i)
    run_once(loop)
Andrew Svetlov
  • 16,730
  • 8
  • 66
  • 69
  • wait... what? How does `run_forever` run only once? How does this make sense? – vitiral Apr 25 '15 at 17:06
  • 1
    @cloudformdesign See my answer [here](http://stackoverflow.com/a/29868627/2073595) for an explanation. – dano Apr 25 '15 at 17:50
  • and that was my question! Thanks! – vitiral Apr 26 '15 at 18:39
  • 1
    After checking the official docs ([here](https://docs.python.org/3.4/library/asyncio-eventloop.html#asyncio.BaseEventLoop.run_forever)), I've confirmed this is in fact the "canonical" way to do this. Thanks! – Lucretiel Feb 23 '16 at 20:40
1

Take a look at this example.

import asyncio
from tkinter import *

class asyncTk(Tk):
    def __init__(self):
        super().__init__()
        self.running = True
        self.protocol("WM_DELETE_WINDOW", self.on_closing)

    def on_closing(self):
        self.running = False
        self.destroy()
        
    def __await__(self):
        while self.running:
            self.update()
            yield

async def asd():
    for x in range(1,10):
        await asyncio.sleep(1)
        print(x)

async def main():
    w = asyncTk()
    asyncio.create_task(asd())
    await w

asyncio.run(main())
The Matrix
  • 21
  • 1
  • This works, but my question already covered it as a possibility. I was asking about using Tk to drive the event loop instead of asyncio. – Lucretiel Sep 02 '20 at 19:43
0

I using the following procedure for own run_once() and run_forever() creation.

Here's a simplified example:

import asyncio

async def worker(**kwargs):
    id = kwargs.get('id', '0.0.0.0.0.0')
    time = kwargs.get('time', 1)

    try:
        # Do stuff.
        print('start: ' + id)
    finally:
        await asyncio.sleep(time)

async def worker_forever(**kwargs):
    while True:
        await worker(**kwargs)

def init_loop(configs, forever=True):
    loop = asyncio.get_event_loop()

    if forever:
        tasks = [
            loop.create_task(worker_forever(id=conf['id'], time=conf['time'])) 
            for conf in config
        ]

    else:
        tasks = [
            asyncio.ensure_future(worker(id=conf['id'], time=conf['time'])) 
            for conf in configs
        ]

    return loop, tasks

def run_once(configs):
    print('RUN_ONCE')
    loop, futures = init_loop(configs, forever=False)
    result = loop.run_until_complete(asyncio.gather(*futures))
    print(result)

def run_forever(configs):
    print('RUN_FOREVER')
    loop, _ = init_loop(configs, forever=True)
    try:
        loop.run_forever()
    except KeyboardInterrupt:
        pass
    finally:
        print("Closing Loop")
        loop.close()

if __name__ == '__main__':
    configurations = [
        {'time': 5, 'id': '4'},
        {'time': 6, 'id': '5'},
        {'time': 1, 'id': '6'},
    ]  # TODO :: DUMMY

    run_once(configurations)
    run_forever(configurations)
Benyamin Jafari
  • 27,880
  • 26
  • 135
  • 150