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()
...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
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, ...).
I had similar task solved with multiprocessing
.
Major parts:
Tk
's process with mainloop
.daemon=True
process with aiohttp
service that executes commands.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