質問をすることでしか得られない、回答やアドバイスがある。

15分調べてもわからないことは、質問しよう!

新規登録して質問してみよう
ただいま回答率
85.35%
Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

Q&A

解決済

2回答

5229閲覧

Python3 Tkinter スレッドで応答なしになる

person

総合スコア224

Python 3.x

Python 3はPythonプログラミング言語の最新バージョンであり、2008年12月3日にリリースされました。

Tkinter

Tkinterは、GUIツールキットである“Tk”をPythonから利用できるようにした標準ライブラリである。

0グッド

1クリップ

投稿2020/10/22 08:58

編集2020/10/26 05:23

Tkinterで画面表示、別スレッドでウィジェットのステータスを変更します。

スレッドを生成、複数のウィジェットのステータスを変更します。
そこで、ウィンドウの閉じるボタンを押すと応答なしになります。

処理が多すぎるのが原因でしょうか?

time.sleep()の値を増やしたら少しは軽くなりましたが、
この値はあまり増やしたくはありません。
(スレッドをもっと生成して、処理を分散させるべき?)

対処方法はありますか?

Windowsです。

Python

1import threading 2import time 3import tkinter as tk 4 5 6def func(): 7 global flag, labels 8 cnt = 0 9 while flag: 10 for i in range(150): 11 labels[i]["text"] = str(cnt) 12 cnt += 1 13 time.sleep(0.1) 14 15def close(): 16 global flag, thread, win 17 flag = False 18 thread.join() 19 win.destroy() 20 21 22win = tk.Tk() 23win.geometry("200x100") 24 25labels = [] 26for i in range(15): 27 for j in range(10): 28 label = tk.Label(win) 29 label.grid(row=i, column=j) 30 labels.append(label) 31 32flag = True 33thread = threading.Thread(target=func) 34thread.start() 35 36win.protocol("WM_DELETE_WINDOW", close) 37win.mainloop()

thread.join()を消すと応答なしにはなりませんが、
ウィンドウを閉じた後にスレッドの処理をしようとしてエラーになったり、
VSCodeで終わっていないような振る舞い(添付画像)をします。

キャプチャ

after()をサブスレッドから呼び出し(これもダメか・・・)

Python

1import tkinter as tk 2import threading 3import queue 4 5def func1(): 6 global flag, labels, win 7 cnt = 0 8 while flag: 9 txt = str(cnt) 10 win.after_idle(lambda:func2(txt)) 11 cnt += 1 12 13def func2(txt): 14 global labels 15 for i in range(150): 16 labels[i]["text"] = txt 17 18def close(): 19 global flag, thread, win 20 flag = False 21 thread.join() 22 win.destroy() 23 24if __name__ == "__main__": 25 26 win = tk.Tk() 27 28 row = 15 29 column = 10 30 labels = [] 31 32 for i in range(row): 33 for j in range(column): 34 label = tk.Label(win) 35 label.grid(row=i, column=j) 36 labels.append(label) 37 38 flag = True 39 thread = threading.Thread(target=func1) 40 thread.start() 41 42 win.protocol("WM_DELETE_WINDOW", close) 43 win.mainloop()

afterの呼び出しをサブスレッドからメインスレッドに変更
(これは上手くいっている?)

Python

1import tkinter as tk 2import threading 3import time 4import queue 5 6def loop(): 7 global data, flag 8 cnt = 0 9 while flag: 10 data.put(str(cnt)) 11 cnt += 1 12 time.sleep(0.1) 13 14def disp(): 15 global data, flag, label1, label2 16 while not data.empty(): 17 tmp = data.get() 18 label1["text"] = tmp 19 label2["text"] = tmp 20 win.after(10, disp) # 指定時間[ms]はloop()の更新時間[s]より短くしないと、while not data.empty()で瞬間的に処理されてしまう 21 22def close(): 23 global flag, thread, win 24 flag = False 25 thread.join() 26 win.destroy() 27 28 29win = tk.Tk() 30 31data = queue.Queue() 32 33label1 = tk.Label(win) 34label1.grid() 35label2 = tk.Label(win) 36label2.grid() 37 38flag = True 39thread = threading.Thread(target=loop) 40thread.start() 41 42# winが存在する(破棄されていない)場合はdisp()を実行 43win.after_idle(disp) # labelを破棄する予定はないので、winでafter設定 44 45win.protocol("WM_DELETE_WINDOW", close) 46win.mainloop()

気になる質問をクリップする

クリップした質問は、後からいつでもMYページで確認できます。

またクリップした質問に回答があった際、通知やメールを受け取ることができます。

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

guest

回答2

0

ベストアンサー

sleep のタイミングで閉じないと終了できないコードになってます。

複数の問題があるので個別に見ていきましょう。
2つの問題があります、


ウィジェットをたくさん作っている為、終了が遅くなる

少しは軽くなった → 終了に時間が掛かるのは、ウィジェットの数が多い為です。

終了時に時間が掛かる問題を再現するコード

python

1import tkinter as tk 2 3root = tk.Tk() 4for _ in range(20000): # <--- ここの値は適宜調整してください 5 label = tk.Label(root) 6 label.place(x=0, y=0) 7 label.place_forget() 8else: 9 print(label) 10tk.Button(root, text="destroy", command=root.destroy).pack() 11tk.Button(root, text="quit", command=root.quit).pack() 12root.mainloop()
  • Python内での label は、次のループで上書きされますが、

tk 側ではラベルは残り続けます。

  • destroy では、全てのウィジェットをプログラム終了前に破棄しようとします
  • quit では先にイベントループを終了します。

これの回避策としては root.quit() の利用がありますが、
対処の方法としては、Canvas を使い独自のウィジェットを実装すると、
一つのウィジェットで済みます。


スレッドからGUIを操作しているので、終了時にデッドロック

  • それとは別に、応答なしになって終了できなくなるのは、

 スレッド内でGUI操作を行っている為。

sleep 中に閉じないと終了できないコードになってます。

sleep を増やすと終了できるようになるのは、
sleep 中に閉じるを押す確率が上がった為です。
再現が難しくなるだけで、タイミング次第では応答なしになります。


わかりやすく問題を再現する為に、
ラベルを変更する前後で sleep を挟んでます。

  • sleep 3秒 <--- before text: ここで閉じると
  • ラベル変更      (thread.join時は) GUIイベントが処理できないデッドロック
  • sleep 3秒 <--- after text: ここで閉じると
  • flag 終了チェック  正常終了

python

1 2 3 4import time 5import threading 6import tkinter as tk 7import logging 8 9flag = True 10 11def worker(): 12 global flag 13 for i in range(5): 14 logging.debug("before text") 15 time.sleep(3) 16 17 # ここで閉じるとデッドロック(応答なし) 18 19 # スレッド内で GUI 操作関連のコードを実行する為には 20 # メインスレッドでmainloop()が稼働していないといけません。 21 # 22 # しかし、メインスレッドでは thread.join でスレッドの終了を待っていて 23 # サブスレッド側では、メインスレッドがイベントループに処理が戻るのを待っています。 24 # どちらも待機状態のまま終わることが無い為、応答なしとなります。 25 label["text"] = str(i) 26 logging.debug(f"text = {i}") 27 28 # ここで閉じると、正常終了 29 30 logging.debug("after text") 31 time.sleep(3) 32 33 if not flag: 34 break 35 36logging.basicConfig( 37 level=logging.DEBUG, 38 format="%(threadName)s %(message)s") 39 40root = tk.Tk() 41label = tk.Label(root) 42label.pack() 43 44thread = threading.Thread(target=worker) 45thread.start() 46 47def close(): 48 global flag 49 flag = False 50 51 # スレッドの終了を待つブロッキング処理 52 logging.debug("thread join") 53 thread.join() 54 55 # スレッドが終了するまでここは実行されない 56 # thread.join により tkinter のイベントループは停止中 57 58 logging.debug("root destroy") 59 root.destroy() 60 61root.protocol("WM_DELETE_WINDOW", close) 62root.mainloop() 63 64

サブスレッドからのスレッドセーフでない操作(ラベルの更新部分)
を排他制御することで回避は出来ますが、コードは複雑化します。
キューを使うと幾分かシンプルにはなりますが。

これの対処法は、GUIのイベントループのタイマーを使い実装する事です。

基本的に、GUIの描画を更新できるのはイベントループを持つスレッドのみなので、
スレッドを増やすと解決出来る問題ではなく、
また、スレッドの用法としても適切ではありません。

スレッドでは(GUIに依存しない部分の)演算のみを行い、
描画は GUI のイベントに任せる、というのが問題の少ない使い方です。


投稿2020/10/23 04:15

編集2020/10/23 04:16
teamikl

総合スコア8760

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

person

2020/10/23 04:26

回答ありがとうございます。 先ほどafter()を使って試してみたのですが、すこしかくつくものの応答なしにはなりませんでした。 GUIのイベントループで処理するのであれば、after()を使った方がいいですかね。 その場合、ラベルのテキスト内容をCSVやiniファイルから拾う場合もこれで大丈夫でしょうか。
teamikl

2020/10/23 04:39

>先ほどafter()を使って試してみたのですが、すこしかくつくものの応答なしにはなりませんでした。 もし、ゲームのような高速なアニメーションを期待してなら タイマーの精度の問題で期待通りの描画更新速度は出ないかもしれません。 ゲーム系のコードでは、意図的にmainloop() は使わず、 任意のタイミングで 頻繁に update() / update_idletasks() を呼び、 イベントを滞らせることなくスムーズなアニメーションを実現するようです。 ---- > その場合、ラベルのテキスト内容をCSVやiniファイルから拾う場合もこれで大丈夫でしょうか。 いいえ。ファイルの読み込み等はスレッドの利用が適切です。 読み込んだ値を加工まではスレッドで担当し、 表示のみメインスレッドへqueue等で渡すようにしてください。 (メインスレッド側のキューの読み出しでもタイマーを使う等、一工夫必要になります)
teamikl

2020/10/23 06:07 編集

補足で、理由の説明 tkinter のタイマーでファイルの読み書きをすると… - ファイルやネットワークなどのIO はブロッキング操作の為、  障害等何らかの理由で、読み込みがうまくいかない&時間が掛かった時  イベントループに処理が戻らず、応答なしになる可能性があります。   - CSVを after() で読み出す場合、  1行ずつ読み込む必要があるのですが、タイマーを挟むと  実質ループ内で time.sleep したような動作になり、  読込速度が遅くなり、十分なパフォーマンスを得られません。 プログラムの冒頭で ini ファイルを読込など、 小規模&用途次第では、実際の運用上問題にならない場合もあるので、 全てのファイル読み書きを別スレッドでとまではいきませんが、 CSVファイル等は、出来るなら別スレッドで処理した方が良いと思います。 追記: 題材のコードではスレッドでのラベル更新だけなので、 ここだけならタイマーでの実装が適切ですが、 実際の用途(CSVファイルの読み込み&GUIの更新 を想定)では、 - ファイル読み込み部分をThread - ファイルから読み込んだデータをキューに入れる - メインスレッドからはタイマーでキューを読み出して ラベルを更新
guest

0

意図的にmainloop() は使わず、

任意のタイミングで 頻繁に update() / update_idletasks() を呼び、
イベントを滞らせることなくスムーズなアニメーションを実現するようです

自己レスですが、動作確認用のコード投稿しておきます。
質問に対する解決策という訳ではありません

注意点: メインスレッド内でループや time.sleep() を使う
というのは、一般的なGUIプログラミング
(イベント駆動プログラミング全般)では避けた方が良いのですが。

この場合の、while True: ~はこれ自体が イベントループの役割をする為、
mainloopは使わない形になります。

もう一つ注意点:
flag でループを抜ける判定の位置について、
このイベントループ内で WM_DELETE_WINDOW の関数が呼ばれるのは
update/update_idletasks の内部です。

close() が呼ばれた後、直ぐに flag 判定が行われるように
ループの終了条件を配置します。

win.destroy() が呼ばれた後 ラベル更新しようとするとエラーになります。

python

1import time 2import tkinter as tk 3 4def main(): 5 flag = True 6 cnt = 0 7 8 win = tk.Tk() 9 win.geometry("200x100") 10 11 def close(): 12 nonlocal flag 13 flag = False 14 win.destroy() 15 16 win.protocol("WM_DELETE_WINDOW", close) 17 18 labels = [] 19 for i in range(15): 20 for j in range(10): 21 label = tk.Label(win) 22 label.grid(row=i, column=j) 23 labels.append(label) 24 25 while True: 26 for i in range(150): 27 if not flag: 28 return 29 labels[i]["text"] = cnt 30 win.update_idletasks() 31 else: 32 win.update() 33 cnt += 1 34 time.sleep(0.1) 35 36 37if __name__ == '__main__': 38 main()

投稿2020/10/23 04:56

編集2020/10/23 04:58
teamikl

総合スコア8760

バッドをするには、ログインかつ

こちらの条件を満たす必要があります。

person

2020/10/23 06:37

知識不足で申し訳ないのですが、 update() と update_idletasks() がどういう関数か教えていただけますか? 関数のコメントに update()には > Enter event loop until all pending events have been processed by Tcl. update_idletasks()には > Enter event loop until all idle callbacks have been called. This will update the display of windows but not process events caused by the user. とありますが、いまいち意味が分からなかったので・・・。
teamikl

2020/10/23 09:02

描画や入力イベントの処理を行う関数です。 mainloop() と同じようなイベントループで、mainloop() との違いは mainloop はウィンドウが閉じられる迄のループ。 update~は、待機中のイベントがなくなるとループを抜けます。 update/update_idletasksの違いは、 update_idletasks だと処理されないイベントがある点です。 上記のコードで意図してる動作は、 update_idletasks でラベルの反映 update で、それ以外のイベントの処理。 (但し、この例のコードでは他のイベントはないので update は何もしません) よくわからない場合は、update_idletasks() で十分です、 不十分な場合のみ update() を使います。 ですが、イベント処理は そもそも mainloop へ処理を戻せばよいので、 極力、メインスレッド内では、他のループ等で処理をブロックするような操作は避けた方が良いです。
person

2020/10/23 11:14

> 上記のコードで意図してる動作は、 > update_idletasks でラベルの反映 > update で、それ以外のイベントの処理。 > (但し、この例のコードでは他のイベントはないので update は何もしません) update_idletasks()などが必要(?)なのはウィジェットの処理が多いからですか? どういう場面に直面した時に必要かどうか判断すればいいのかわかりません。 あと、スレッドでファイル読み込み→キュー→メインスレッドでラベル変更についてはteamikl様の上記ソースコードを参考にしてみたいと思います。 今のところ、まだうまくできていないのですが、イメージ的にはこんな感じですかね(質問文に追記)?
teamikl

2020/10/23 11:45

>update_idletasks()などが必要(?)なのはウィジェットの処理が多いからですか? >どういう場面に直面した時に必要かどうか判断すればいいのかわかりません。 通常は、update ~はあまり使わずに済ませるのが良いです。 update 自身もループなので、mainloopの中でループというのは あまり良い方法ではありません。 デバッグ時のスタックトレース情報が深くなり、 問題を探すのが複雑なる傾向になります。 このコードはスレッドを使わず メインスレッドで mainloop の代わりに 独自にイベントループを組んでいる為、update~を使ってます。 このコード自体は、今回の質問に対する解決策としてではなく、 その後のコメントの流れで提示した例と言うのを了承ください。 上記のアプローチでのイベントループ構築は - ゲーム系のコード - 他のGUIの埋め込み - 非同期ライブラリ asyncio を使ったコード 等で、mainloopを使えない(他方もイベントループを持つ)状況があるので、 そういった場合に、独自のイベントループを構築する為に用います。 他には、個人的にはお勧めな方法ではありませんが - 「メインスレッドで」時間の掛かるループを処理したい時 for line in 大きなファイル読み込み:  do_something(line)  root.update() # <-- ファイル読み込み梅雨に「閉じる」等のイベントが処理されるようにする こういった場合は、大抵スレッドで処理した方がよいです。 (update/update_idletasks 使い分けは、 他で使ってるイベント次第なので実際に試してください。)
teamikl

2020/10/23 12:34

>イメージ的にはこんな感じですかね(質問文に追記)? この私の回答のコード (update を用いたループ) は、不要です。 普通は、mainloop を使う方法で大丈夫です。 サブスレッド→メインスレッドのキューの読み出しに関しては いろいろな方法があるので、どの方法が適切なのかは状況次第ですが、 - after_idle を使う queue の代わりに、サブスレッドから after_idle を使う方法 タイマーに登録された関数は、mainloop で呼び出す為、 メインスレッドで実行されます。 - 定期的にタイマーでキューを読み出す ファイル読み込み等の用途には合いませんが、 処理速度が重要でない場合等では使える方法。 キューの読み出しでは、キューが空の時は待機してしまわないように ノンブロッキングモード(get_nowait())にします。 - event_generate で通知する queueにput した後に、event_generate で独自イベントを生成します。 bind で独自イベントに対して、キューから読み出しを行う関数を登録します。 # sub-thread側 queue.put(item) root.event_generate("<<QueuePut>>") # main-thread側 def process_queue(event):  item = queue.get()  ... root.bind("<<QueuePut>>", process_queue)
person

2020/10/24 16:15

一旦、mainloop()を使う方法でもう少し試してみます。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

15分調べてもわからないことは
teratailで質問しよう!

ただいまの回答率
85.35%

質問をまとめることで
思考を整理して素早く解決

テンプレート機能で
簡単に質問をまとめる

質問する

関連した質問