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

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

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

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

Tkinter

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

Q&A

解決済

1回答

2389閲覧

Python3 Tkinter サブスレッドで処理してるときにUIを無効にしたい

person

総合スコア223

Python 3.x

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

Tkinter

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

0グッド

0クリップ

投稿2021/08/24 05:51

編集2021/08/24 06:19

Tkinterで次のようなアプリを作りたいです。

ボタンを押すと、エントリの文字列をCSVに書き込む。
ただし書き込み中は、画面の操作を不可能(state -> disable)にする。
書き込みが終わったら、画面の操作を可能(state -> normal)にする。
(仕様について、なぜ画面の操作を不可能にするかなどは気にしないでください。)

ソースコードを下記のように作りましたが、一つ気になることがあります。

メインスレッド->サブスレッドはサブスレッドで都度キューのデータの取得をすればいいらしいですが、
サブスレッド->メインスレッドの場合はafter()を使えばいいのでしょうか?
(サブスレッドの処理のタイミングでUIを操作するため、コマンドとしてキューでメッセージを渡す。)

Python

1import csv 2import os 3import queue 4import sys 5import threading 6import time 7import tkinter as tk 8from datetime import datetime 9from tkinter import messagebox 10 11 12def on_thread1(): 13 global loop_flag, from_main_to_sub, from_sub_to_main 14 15 while loop_flag: 16 while not from_main_to_sub.empty(): 17 data = from_main_to_sub.get_nowait() 18 if data[0] == "WRITE_CSV": 19 from_sub_to_main.put("STATE_DISABLED") 20 time.sleep(1) # UI無効確認用 21 txt = data[1] 22 write_csv(txt) 23 from_sub_to_main.put("STATE_NORMAL") 24 25 26def write_csv(txt): 27 dir_ = os.path.abspath(os.path.dirname(sys.argv[0])) 28 fle = os.path.join(dir_, "test.csv") 29 30 with open(fle, "a", encoding="utf_8", newline="") as wf: 31 writer = csv.writer(wf) 32 writer.writerow([datetime.now(), txt]) 33 34 35def on_after(): 36 global from_sub_to_main 37 38 while not from_sub_to_main.empty(): 39 data = from_sub_to_main.get_nowait() 40 if data == "STATE_DISABLED": 41 entry.configure(state = "disabled") 42 button.configure(state = "disabled") 43 elif data == "STATE_NORMAL": 44 entry.configure(state = "normal") 45 button.configure(state = "normal") 46 entry.delete(0, "end") 47 else: 48 pass 49 50 root.after(100, on_after) 51 52 53def on_button(): 54 global from_main_to_sub 55 56 txt = entry.get() 57 if txt: 58 from_main_to_sub.put(["WRITE_CSV", txt]) 59 else: 60 messagebox.showerror("Error", "Please enter.") 61 62 63def on_close(): 64 global loop_flag, thread1 65 66 loop_flag = False 67 thread1.join() 68 root.destroy() 69 70 71loop_flag = True 72from_main_to_sub = queue.Queue() 73from_sub_to_main = queue.Queue() 74 75thread1 = threading.Thread(target=on_thread1) 76thread1.start() 77 78root = tk.Tk() 79root.geometry("300x200") 80root.protocol("WM_DELETE_WINDOW", on_close) 81 82entry = tk.Entry(root) 83entry.grid() 84 85button = tk.Button(root, text="Button", width=10, command=on_button) 86button.grid() 87 88root.after_idle(on_after) 89 90root.mainloop()

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

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

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

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

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

guest

回答1

0

ベストアンサー

queue.put に関して

サブスレッド->メインスレッドの場合はafter()を使えばいいのでしょうか?

追記: queue.get での after に関しては後述

after, after_idle で、「対象の関数をメインスレッド側で呼び出させること」はできます。
大抵の場合、サブスレッド→メインスレッドの queue の代替として使えます。

デメリットや懸念事項は、

  • 「after の呼び出し自体」がスレッドセーフでない可能性があります。
  • after が tkitner 依存なので

 例えば、別のGUIに移植といった場面を想定するなら、
別途 サブスレッド→メインスレッド用の queue を用意する方が、
再利用性は高まります。


tkinter がスレッドセーフかそうでないかは、
一応、ドキュメントには言及があるのですが、

内部モジュール _tkinter は、Python と Tcl とがやり取りできるスレッドセーフなメカニズムを提供しています。

は、内部モジュール _tkinter の話で、
通常の利用での tkinter には、必ずしも当てはまるとは限りません。

tclとやり取りしてる部分のみがスレッドセーフと保証されています。

Python - (thread safe ? ※1) - tkinter - _tkinter <= (thread-safe) => tcl

※1 の部分については、外部ライブラリでスレッドセーフにする方法が幾つかあります。
サブスレッドからのtkinterのメソッド呼び出しを、
内部で queue を使ったメッセージに変換するといったものです。
但し、現行で保守されてるものがあるかは、使ったことがないので知りません。


tkinter と thread については、議論があって
https://bugs.python.org/issue33479

該当する部分を拾い上げると、
確実に安全な方法として推奨されてるのは別途 Queue を明示的に使う方法です。

そうでない場合は、依存ライブラリの tcl がコンパイルされた時の
スレッドオプションの有無に影響。
例えば、tcl のライブラリを含まずにアプリケーションを配布を想定する場合に、
問題となる可能性があります。


queue.get に関して

サブスレッドでメッセージをput、メインスレッドでgetする。

このキューはメインスレッドでafterを常に回すことでgetする、という方法自体(質問文のコード)は問題ないでしょうか。

頻度次第です。

  • OK: queue.get() ではブロッキングの可能性がある為、get_nowait を使う。

  • afterの呼び出し間隔次第でラグが問題にならない様なら大丈夫です。

  • 間隔が短い(高頻度に呼び出される)事による負荷が、

 実行環境次第で問題になる事はあるかもしれません。

 100ms 程度(10回/1秒) であれば問題ないと思いますが、
ユーザーがラグと認識しない程度に増やしても大丈夫です。
(過剰なケース: 1ms 間隔で呼び出し → 不要な呼び出しが大量発生)

懸念事項:

  • empty() を確認してから get 迄に別の場所で読み取られていた場合、エラーになる。

 現状のコードではマルチスレッドを考慮しなくても良い為、問題なしですが、
empty は確認せずに、get 時の例外で queue.Empty を捕捉する方が汎用的です。

while not from_sub_to_main.empty():

がキューへのputが読み出し速度を上まわる場合、
実質無限ループとなり、GUIが応答なしとなる原因に成り得ます。

質問のコードの頻度の queue.put では問題ありませんが、
例えば、動画のフレームをqueueでやる取する場合は調整が必要です。

ライブラリ化して、汎用的なコードにしたい場合は対策が必要。
現状のコードでしか使わない実装なら対策不要。

対策する場合、一度のafterで読み出せる回数に制限を設けます。例)

python

1for _ in range(5): 2 if queue.empty() 3 break 4 ...

after 以外でキューの読み取り手段もありますが、
お勧めしにくい理由として、サンプルコードをほぼ見かけたことありません。
出典を出せればよいのですが…

  • A: サブスレッドで event_generate を使い通知、

 メイン側で bind した任意の関数を呼び出す。
※ event_generate は thread-safe な関数

  • B: 非同期IO (asyncio) で動かす。

 利点は、Event 等、queue 以外の排他制御の手段が取れる。
但し、プログラム全体を asyncio に対応させる必要あり。

  • C: createfilehandler を用いた通知

解説
after での実行のように、
ループで毎回データが届いているか確認する方式を polling といい、
こういった方式での実装に共通した、問題点・改善方法が知られています。
上に示した方法は、何れもこの問題を改良・効率化する方法です。

after 100ms では、約10回/1秒 程度の頻度での呼び出しですが、
動画再生等の高頻度のやり取りでもない限り、大半が不要な呼び出しとなります。

event_generate では、通知を <Button> 等と同じ GUI のイベントとして得られます。
asyncio は、(内部でpollingに近いことは行われていますが)より効率的な方法で実装されてます。
createfilehandler も同様、通知の仕組みの一種です。
polling とは異なり、通知のタイミングで任意の関数を呼び出せます。
→ 不要な呼び出し回数を抑制できる。

但し、アプリ-ケーション単位で見る場合、
この部分の改善はユーザ視点では目に見えにくく、

ライブラリとして実装していて汎用的な実装にしたい場合や、
組み込み環境で消費電力が気になる、
等といった事情でもない限り優先度は低めになると思います。


追記2: 実際のアプリケーションでの after を用いた queue の読み出し

  • Tkinter製IDE: IDLE のコード (idlelib.rpc) pollresponse 内で queue.get

 idlelib.pyshell poll_subprojecc内でafter

  • Tkinter製IDE: Thonny のコード (thonny.workbench)

  _poll_ipc_requests

投稿2021/08/24 07:57

編集2021/08/25 10:42
teamikl

総合スコア8664

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

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

person

2021/08/24 15:59

回答の内容について、 afterの呼び出し元がサブスレッドの場合の話でしょうか。 > 確実に安全な方法として推奨されてるのは別途 Queue を明示的に使う方法です。 サブスレッドでメッセージをput、メインスレッドでgetする。 このキューはメインスレッドでafterを常に回すことでgetする、という方法自体(質問文のコード)は問題ないでしょうか。
teamikl

2021/08/25 02:15

> 回答の内容について、 > afterの呼び出し元がサブスレッドの場合の話でしょうか。 queue.put の代替として after を使う事を想定しての回答でした。 queue.get 側の様ですね。回答に追記します。
person

2021/08/25 12:45

ありがとうございます。 afterの時間はユーザが遅いと思わなければいいと思っているので、0.5~1sぐらいに設定することにします。 ほかの方法もあるんですね。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

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

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

質問する

関連した質問