0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

asyncioとtkinterの共存

Posted at

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は実行され続け、問題は見つかっていません。

参考文献

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?