TL;DR
tkinterの定期タスク上でasyncioを1サイクルのみ実行します。
import tkinter as tk
import asyncio
loop = asyncio.new_event_loop()
def do_async_tasks(loop):
loop.call_soon(loop.stop)
loop.run_forever() # execute one cycle only
master.after(100, do_async_tasks, loop)
master = tk.Tk()
var = tk.StringVar(master)
tk.Label(master, textvariable=var, width=10).pack()
async def update(var):
for i in range(5):
await asyncio.sleep(1)
var.set(str(i))
loop.create_task(update(var))
master.after(100, do_async_tasks, loop)
master.mainloop()
背景
GUI表示(tkinter)とI/O待ちを伴う処理がありました。GUIの応答性を上げるためI/O待ちをバックグラウンド処理として並行処理したいと考えました。
一般には並列処理(スレッドなど)で並行処理を実現しますが、様々な面倒事(再現性の低い不具合など)を伴うので避けたいです。
そこで、asyncioを用い非並列処理な並行処理を用いたいと考えました。
しかし、tkinterのmainloop()
はブロック処理でありコルーチン上で実行できない=asyncioと共存できない、はてどうしよう…
試行錯誤
方法1: tkinterスレッドとasyncioスレッドを分離する
メインスレッドでtkinterを実行、別スレッドでasyncioを実行し、スレッドセーフな協調メカニズムで連携します。
tkinter→asyncioの協調はasyncio.get_event_loop().loop.call_soon_threadsafe(...)
です。
asyncio→tkinterの協調はmaster.after(...)
です。
import threading
import tkinter as tk
import asyncio
master = tk.Tk()
var = tk.StringVar(master)
tk.Label(master, textvariable=var, width=10).pack()
async def update(master, var):
for i in range(5):
await asyncio.sleep(1)
master.after_idle(var.set, str(i))
loop = asyncio.new_event_loop()
loop.create_task(update(master, var))
t = threading.Thread(target=loop.run_forever, daemon=True)
t.start()
master.mainloop()
問題
「結局スレッドをつかっているよね?」はい、使っています。うっかりtkinterスレッドからasyncioスレッドのオブジェクトを操作すると不可解な挙動をします。asyncio利用の動機(並列処理を避けたい)を達成していません。
また、原因を特定出来ていませんが特定の処理をasyncioのタスクに入れたら描画を目視できるほどGUIの応答性が著しく低下しました。
方法2: asyncio上でtkinterを動かす
tkinterのmaster.mainloop()
の代わりに、master.update()
を定期的に呼びます。
import tkinter as tk
import asyncio
master = tk.Tk()
var = tk.StringVar(master)
tk.Label(master, textvariable=var, width=10).pack()
async def update(master, var):
for i in range(5):
await asyncio.sleep(1)
master.after_idle(var.set, str(i))
loop = asyncio.new_event_loop()
loop.create_task(update(master, var))
async def async_mainloop(master, loop):
master.wm_protocol("WM_DELETE_WINDOW", loop.stop)
while True:
master.update()
await asyncio.sleep(0.1)
loop.create_task(async_mainloop(master, loop))
loop.run_forever()
問題
致命的な欠陥があります。それはtkinterがブロック処理を含むことです。
ドラッグ中、ウィンドウ移動中の間、master.update()
は処理をブロックします。
またダイアログ完了待ち処理wait_window()
も処理をブロックします。
これらブロック中は他のasyncioタスクが停止します。
方法3: tkinter上でasyncioを動かす
冒頭で述べた方法です。
tkinterのブロック処理中でもafterは実行され続け、問題は見つかっていません。