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

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

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

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

Tkinter

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

メモリリーク

メモリリークは、プログラムファイルがメモリの解放に失敗した時に起こります。

ループ

ループとは、プログラミングにおいて、条件に合致している間、複数回繰り返し実行される箇所や、その制御構造を指します

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

Q&A

解決済

2回答

6158閲覧

Tcl_AsyncDelete: async handler deleted by the wrong thread 対策

sobagome

総合スコア9

Python 3.x

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

Tkinter

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

メモリリーク

メモリリークは、プログラムファイルがメモリの解放に失敗した時に起こります。

ループ

ループとは、プログラミングにおいて、条件に合致している間、複数回繰り返し実行される箇所や、その制御構造を指します

Python

Pythonは、コードの読みやすさが特徴的なプログラミング言語の1つです。 強い型付け、動的型付けに対応しており、後方互換性がないバージョン2系とバージョン3系が使用されています。 商用製品の開発にも無料で使用でき、OSだけでなく仮想環境にも対応。Unicodeによる文字列操作をサポートしているため、日本語処理も標準で可能です。

1グッド

2クリップ

投稿2021/04/09 01:19

前提・実現したいこと

GUIを「停止」ボタンを押さずに右上の×で閉じると、ループが動作し続けてしまうので、
×を押すと停止するようにさせたい。
また、スレッドが並列で立ち上がっているからなのか、何回か実行を繰り返していると、
下記エラーが発生しカーネルが停止してしまうので対策を立てたい。

Tcl_AsyncDelete: async handler deleted by the wrong thread

該当のソースコード

python

1# -*- coding: utf-8 -*- 2import tkinter as tk 3import threading 4 5class MainWin(tk.Frame): 6 def __init__(self,master): 7 super().__init__(master) 8 self.grid() 9 self.master.geometry("100x50") 10 self.master.title("MainWindow") 11 self.create_widgets() 12 self._resetbutton() 13 14 def create_widgets(self): 15 # 実行ボタンの作成 16 self.button_run = tk.Button(self, text="実行", command=self.mainstart) 17 self.button_run.grid(row=0, column=0) 18 # 停止ボタンの作成 19 self.button_stop = tk.Button(self, text="停止", command=self._resetbutton) 20 self.button_stop.grid(row=0, column=1) 21 22 def _resetbutton(self): 23 self.running = False 24 self.button_run.config(text="実行", command=self.startthread,state=tk.NORMAL) 25 26 def startthread(self): 27 self.running = True 28 newthread = threading.Thread(target=self.mainstart) 29 newthread.start() 30 self.button_run.config(text="実行", state=tk.DISABLED) 31 self.button_stop.config(text="停止", command=self._resetbutton) 32 33 def mainstart(self): 34 while self.running: 35 print("start") 36 print("stop") 37 38def main(): 39 root = tk.Tk() 40 app = MainWin(master=root) 41 app.mainloop() 42 43if __name__ == "__main__": 44 main()

補足情報(FW/ツールのバージョンなど)

Python 3.8.3
Spyder 4.1.4

teamikl👍を押しています

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

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

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

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

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

guest

回答2

0

ベストアンサー

エラーについては再現できませんが、
スレッドで実行してる関数内で、tkinter にアクセスしてませんか?


簡単な回避策: デーモンスレッドにする

スレッド内でリソース等の後始末が不要な場合にのみ使えます。

python

1newthread = threading.Thread(target=self.mainstart, daemon=True)

他のスレッドがなくなった場合、自動的に破棄されるようになるので、
単純なケースでは、プログラム終了時に破棄されます。

但し、mainstart の内容次第では、適切な解決策でない場合もあり。


解決策: スレッドをインスタンス変数に格納し、後始末をきちんとする。

右上のボタンを押したときの挙動については

root.protocol("WM_DELETE_WINDOW", cload_callback_func)

で終了前に呼び出す関数を登録できます。

スレッドの後始末の手順は、

  • 準備: 別のメソッドからもスレッドを参照したいので、

 スレッドをインスタンス変数にしておく newthread -> self.thread

  • ループ終了フラグを変更して
  • スレッドをjoin() で完了を待つ

 但し、thread.join はメインスレッドで実行することになりますが、
GUI が稼働中に呼び出すとGUIがフリーズするので、呼び出すタイミングには注意。

※ 例えば、停止処理の _resetbutton() は tk のボタンを参照してるので、
後始末処理の中では呼び出してはいけません。
スレッドの完了を待っている間は GUI は動かない。

Tcl_AsyncDelete については、
サブスレッドでの処理の内容や後始末の仕方次第では起こり得ますが、
(再現できなかったため詳細は解りません)

質問の右上のウィンドウを閉じて終了した場合の対策は、以上で出来るはずです。


diff

1--- main.py 2021-04-09 16:45:01.292878500 +0900 2+++ test03.py 2021-04-09 17:11:24.982730000 +0900 3@@ -5,6 +5,7 @@ 4 class MainWin(tk.Frame): 5 def __init__(self,master): 6 super().__init__(master) 7+ self.thread = None 8 self.grid() 9 self.master.geometry("100x50") 10 self.master.title("MainWindow") 11@@ -13,7 +14,7 @@ 12 13 def create_widgets(self): 14 # 実行ボタンの作成 15- self.button_run = tk.Button(self, text="実行", command=self.mainstart) 16+ self.button_run = tk.Button(self, text="実行", command=self.startthread) 17 self.button_run.grid(row=0, column=0) 18 # 停止ボタンの作成 19 self.button_stop = tk.Button(self, text="停止", command=self._resetbutton) 20@@ -21,23 +22,32 @@ 21 22 def _resetbutton(self): 23 self.running = False 24- self.button_run.config(text="実行", command=self.startthread,state=tk.NORMAL) 25+ self.button_run.config(state=tk.NORMAL) 26 27 def startthread(self): 28 self.running = True 29- newthread = threading.Thread(target=self.mainstart, daemon=True) 30+ newthread = threading.Thread(target=self.mainstart) 31 newthread.start() 32- self.button_run.config(text="実行", state=tk.DISABLED) 33- self.button_stop.config(text="停止", command=self._resetbutton) 34+ self.thread = newthread 35+ self.button_run.config(state=tk.DISABLED) 36+ self.button_stop.config(state=tk.NORMAL) 37 38 def mainstart(self): 39 while self.running: 40 print("start") 41 print("stop") 42 43+ def cleanup(self): 44+ if self.thread: 45+ self.running = False 46+ self.thread.join() 47+ self.thread = None 48+ self.master.destroy() # self.master must be root object 49+ 50 def main(): 51 root = tk.Tk() 52 app = MainWin(master=root) 53+ root.protocol("WM_DELETE_WINDOW", app.cleanup) 54 app.mainloop() 55 56 if __name__ == "__main__": 57 58 59

投稿2021/04/09 08:28

teamikl

総合スコア8664

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

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

sobagome

2021/04/10 02:51

詳細に解説頂きありがとうございます。 root.protocol("WM_DELETE_WINDOW", cload_callback_func) を組み入れて右上の×ボタンを押すと、ループの動作が停止するようになりました。 頂いたコードを微修正して解決方法に再度コードを追記しました。 ただ、やはりGUIを立上げ→実行釦を押してループ中に×を押してGUIを閉じる→再度GUIを立ち上げる→実行釦を押すと、 Tcl_AsyncDelete: async handler deleted by the wrong thread 上記エラーが発生します。 >スレッドで実行してる関数内で、tkinter にアクセスしてませんか? この助言に当たる内容が恐らく本質なのだと思いますが、現状のコードにも存在しているのでしょうか。 startthread関数内の下記2行がtkinterにアクセスしているものかと思い、 self.button_run.config(state=tk.DISABLED) self.button_stop.config(state=tk.NORMAL) を除外してみましたが、結果は同じでした。
teamikl

2021/04/10 03:22

>ループ中に×を押してGUIを閉じる→再度GUIを立ち上げる→実行釦を押す 一点、把握漏れがありました。 確認: - サブウィンドウではなく、メインウィンドウを閉じて一旦そのプロセスを終了した状態から  再度立ち上げた場合でしょうか? - Spyder ではなく、端末での実行で問題の動作は確認できますか?  こちらで確認したのは端末での実行のみでした。 コードは見た感じ問題なさそうなので、実行環境の問題かもしれません。
teamikl

2021/04/10 04:17 編集

もう一点見逃してました、カーネルというのは Spyder のカーネルですよね。 関連のありそうな issue https://github.com/spyder-ide/spyder/issues/14627 日本語の場合: ツール→設定→IPythonコンソール→グラフィクスtab→グラフィクスのバックエンド を試してみてください。 ---- >startthread関数内の下記2行がtkinterにアクセスしているものかと思い、 > ~を除外してみましたが、結果は同じでした。 startthread 関数はメインスレッドで呼ばれるので問題ありません。 mainstart 関数がPythonのサブスレッドで実行される関数ですが、 Tcl_AsyncDelete に関しては、Pythonのコードが実行されるスレッドではなく、 Tcl のコードのスレッドの問題なので、ここに関しては別問題でした。
sobagome

2021/04/10 11:31

>サブウィンドウではなく、メインウィンドウを閉じて一旦そのプロセスを終了した状態から再度立ち上げた場合でしょうか? -メインウインドウを閉じてプロセスを終了して、再度立上げた状態です。 > Spyder ではなく、端末での実行で問題の動作は確認できますか?こちらで確認したのは端末での実行のみでした。 -コマンドプロンプトでの実行が上手くいかないので、jupyter notebookで実行を試しましたが、下記コメントが発生し停止しました。 The kernel appears to have died. It will restart automatically. >日本語の場合: ツール→設定→IPythonコンソール→グラフィクスtab→グラフィクスのバックエンドを試してみてください。 - 設定がインラインになっていたので、リンク先に従って「自動」、若しくは「Tkinter」の2パターン適用して試しましたが、結果は同じくTcl_AsyncDelete が発生しました。 -解決には至っていませんが、おっしゃるように実行環境の問題のような気がします。
teamikl

2021/04/10 13:01

>コマンドプロンプトでの実行が上手くいかないので こちらを先に詰めるべきですね。 ちなみに、実際に実行しているコードと質問に掲載のコードは同じものですか?(確認点) jupyter notebook で実行する場合、 もう少し、jupyter や tkinter の構成を把握する必要があって ノートブック等の対話環境ではコード断片を 後から任意のタイミングで実行できるので、 稼働中のプロセスに対してコードを送るといった方式を取ってます。 通常の端末からの実行であれば、 プロセス終了時にソースの解放が行われ、 2度目の実行では新しい環境での実行になりますが、 対話実行の環境では、プロセスを維持し続ける必要がある為 「ウィンドウを閉じる」操作が、プロセスの終了に直結しません。 回答に書いた、デーモンスレッドも意図したタイミングに終了されないと思います。 繰り返し実行したい場合は、通常の実行であれば自動的に行われるような リソースの解放等の後始末を自分で明示的に行う必要があることも有ります。 →再度立ち上げた場合に問題が起こる事から、ここの問題が濃厚です。 (リソース: 具体的には Tkinter 内部のスレッド) ---- https://bugs.python.org/issue39093 問題の再現自体が出来てないので、解決できるか確証はありませんが、 終了時に quit() を呼び出してみてください。 - A案: self.master.destroy() 後に self.master.quit() - B案: app.mainloop() 後に root.quit() 後始末の案(リソースの解放) main()関数の最後に root = app = None import gc gc.collect()
sobagome

2021/04/10 14:25

>コマンドプロンプトでの実行が上手くいかないので >こちらを先に詰めるべきですね。 >ちなみに、実際に実行しているコードと質問に掲載のコードは同じものですか?(確認点) -実際に実行しているコードも質問掲載コードと同一のものです。ちなみに、解決してコマンドプロンプトからも実行を試すことができました。その場合はTcl_AsyncDelete は出ませんでした。 >後始末の案(リソースの解放) main()関数の最後に >root = app = None >import gc >gc.collect() -こちらを追記することにより、Spyder及びjupyter notebook 環境でもTcl_AsyncDelete が出ないことを確認できました!ガベージコレクションによるリソースの解放を明示する必要があるということですね。 -以上より、私の質問に対して全て解決頂く事が出来ました。理由まで記載頂いたので、初学者の私にとっては非常に勉強になりました。何度も読み直して理解を深めたいと思います。ご丁寧にご回答頂きありがとうございました。
guest

0

python

1# -*- coding: utf-8 -*- 2 3import tkinter as tk 4import threading 5import gc 6 7class MainWin(tk.Frame): 8 def __init__(self,master): 9 super().__init__(master) 10 self.thread = None 11 self.grid() 12 self.master.geometry("100x50") 13 self.master.title("MainWindow") 14 self.create_widgets() 15 16 def create_widgets(self): 17 # 実行ボタンの作成 18 self.button_run = tk.Button(self, text="実行", command=self.startthread) 19 self.button_run.grid(row=0, column=0) 20 # 停止ボタンの作成 21 self.button_stop = tk.Button(self, text="停止", command=self._resetbutton) 22 self.button_stop.grid(row=0, column=1) 23 24 def _resetbutton(self): 25 self.running = False 26 self.button_run.config(state=tk.NORMAL) 27 28 def startthread(self): 29 self.running = True 30 newthread = threading.Thread(target=self.mainstart) 31 newthread.start() 32 self.thread = newthread 33 self.button_run.config(state=tk.DISABLED) 34 self.button_stop.config(state=tk.NORMAL) 35 36 def mainstart(self): 37 while self.running: 38 print("start") 39 print("stop") 40 41 def cleanup(self): 42 if self.thread: 43 self.running = False 44 self.thread.join() 45 self.thread = None 46 self.master.destroy() # self.master must be root object 47 48 49def main(): 50 root = tk.Tk() 51 app = MainWin(master=root) 52 root.protocol("WM_DELETE_WINDOW", app.cleanup) 53 app.mainloop() 54 root = app = None 55 gc.collect() 56 57if __name__ == "__main__": 58 main() 59 60

投稿2021/04/10 02:34

編集2021/04/10 14:11
sobagome

総合スコア9

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

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

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.50%

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

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

質問する

関連した質問