Search code examples
pythonuser-interfaceasynchronoustkinterpython-asyncio

Use asyncio and Tkinter (or another GUI lib) together without freezing the GUI


I want to use asyncio in combination with a tkinter GUI. I am new to asyncio and my understanding of it is not very detailed. The example here starts 10 task when clicking on the first button. The task are just simulating work with a sleep() for some seconds.

The example code is running fine with Python 3.6.4rc1. But the problem is that the GUI is freezed. When I press the first button and start the 10 asyncio-tasks I am not able to press the second button in the GUI until all tasks are done. The GUI should never freeze - that is my goal.

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

from tkinter import *
from tkinter import messagebox
import asyncio
import random

def do_freezed():
    """ Button-Event-Handler to see if a button on GUI works. """
    messagebox.showinfo(message='Tkinter is reacting.')

def do_tasks():
    """ Button-Event-Handler starting the asyncio part. """
    loop = asyncio.get_event_loop()
    try:
        loop.run_until_complete(do_urls())
    finally:
        loop.close()

async def one_url(url):
    """ One task. """
    sec = random.randint(1, 15)
    await asyncio.sleep(sec)
    return 'url: {}\tsec: {}'.format(url, sec)

async def do_urls():
    """ Creating and starting 10 tasks. """
    tasks = [
        one_url(url)
        for url in range(10)
    ]
    completed, pending = await asyncio.wait(tasks)
    results = [task.result() for task in completed]
    print('\n'.join(results))


if __name__ == '__main__':
    root = Tk()

    buttonT = Button(master=root, text='Asyncio Tasks', command=do_tasks)
    buttonT.pack()
    buttonX = Button(master=root, text='Freezed???', command=do_freezed)
    buttonX.pack()

    root.mainloop()

A _side problem

...is that I am not able to run the task a second time because of this error.

Exception in Tkinter callback
Traceback (most recent call last):
  File "/usr/lib/python3.6/tkinter/__init__.py", line 1699, in __call__
    return self.func(*args)
  File "./tk_simple.py", line 17, in do_tasks
    loop.run_until_complete(do_urls())
  File "/usr/lib/python3.6/asyncio/base_events.py", line 443, in run_until_complete
    self._check_closed()
  File "/usr/lib/python3.6/asyncio/base_events.py", line 357, in _check_closed
    raise RuntimeError('Event loop is closed')
RuntimeError: Event loop is closed

Multithreading

Whould multithreading be a possible solution? Only two threads - each loop has it's own thread?

EDIT: After reviewing this question and the answers it is related to nearly all GUI libs (e.g. PygObject/Gtk, wxWidgets, Qt, ...).


Solution

  • I had similar task solved with multiprocessing.

    Major parts:

    1. Main process is Tk's process with mainloop.
    2. daemon=True process with aiohttp service that executes commands.
    3. Intercom using duplex Pipe so each process can use it's end.

    Additionaly, I'm making Tk's virtual events to simplify massage tracking on app's side. You will need to apply patch manually. You can check python's bug tracker for details.

    I'm checking Pipe each 0.25 seconds on both sides.

    $ python --version
    Python 3.7.3
    

    main.py

    import asyncio
    import multiprocessing as mp
    
    from ws import main
    from app import App
    
    
    class WebSocketProcess(mp.Process):
    
        def __init__(self, pipe, *args, **kw):
            super().__init__(*args, **kw)
            self.pipe = pipe
    
        def run(self):
            loop = asyncio.get_event_loop()
            loop.create_task(main(self.pipe))
            loop.run_forever()
    
    
    if __name__ == '__main__':
        pipe = mp.Pipe()
        WebSocketProcess(pipe, daemon=True).start()
        App(pipe).mainloop()
    

    app.py

    import tkinter as tk
    
    
    class App(tk.Tk):
    
        def __init__(self, pipe, *args, **kw):
            super().__init__(*args, **kw)
            self.app_pipe, _ = pipe
            self.ws_check_interval = 250;
            self.after(self.ws_check_interval, self.ws_check)
    
        def join_channel(self, channel_str):
            self.app_pipe.send({
                'command': 'join',
                'data': {
                    'channel': channel_str
                }
            })
    
        def ws_check(self):
            while self.app_pipe.poll():
                msg = self.app_pipe.recv()
                self.event_generate('<<ws-event>>', data=json.dumps(msg), when='tail')
            self.after(self.ws_check_interval, self.ws_check)
    

    ws.py

    import asyncio
    
    import aiohttp
    
    
    async def read_pipe(session, ws, ws_pipe):
        while True:
            while ws_pipe.poll():
                msg = ws_pipe.recv()
    
                # web socket send
                if msg['command'] == 'join':
                    await ws.send_json(msg['data'])
    
                # html request
                elif msg['command'] == 'ticker':
                    async with session.get('https://example.com/api/ticker/') as response:
                        ws_pipe.send({'event': 'ticker', 'data': await response.json()})
    
            await asyncio.sleep(.25)
    
    
    async def main(pipe, loop):
        _, ws_pipe = pipe
        async with aiohttp.ClientSession() as session:
            async with session.ws_connect('wss://example.com/') as ws:
                task = loop.create_task(read_pipe(session, ws, ws_pipe))
                async for msg in ws:
                    if msg.type == aiohttp.WSMsgType.TEXT:
                        if msg.data == 'close cmd':
                            await ws.close()
                            break
                        ws_pipe.send(msg.json())
                    elif msg.type == aiohttp.WSMsgType.ERROR:
                        break