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

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

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

MatplotlibはPythonのおよび、NumPy用のグラフ描画ライブラリです。多くの場合、IPythonと連携して使われます。

Tkinter

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

Python

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

Q&A

解決済

2回答

6429閲覧

threadでmatplotlib時のエラー対処法

NKJSM

総合スコア58

Matplotlib

MatplotlibはPythonのおよび、NumPy用のグラフ描画ライブラリです。多くの場合、IPythonと連携して使われます。

Tkinter

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

Python

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

0グッド

1クリップ

投稿2021/10/14 08:03

編集2021/10/15 08:02

Pythonでリアルタイム解析を行うためにthreadを使って図の更新とボタンの監視をしているのですが、右上×ボタンを押したときに
Exception ignored in: <function Image.del at 0x02D448E0>
Traceback (most recent call last):
File "C:\Users\xxxxxxxxxx\AppData\Local\Programs\Python\Python38-32\lib\tkinter_init_.py", line 4014, in del
RuntimeError: main thread is not in main loop
Tcl_AsyncDelete: async handler deleted by the wrong thread
と言われてしまいます。
これの対処法は何かございますでしょうか?

問題点となる箇所のみ取り出してコードを記述させていただきます。

Python

1 # ライブラリ 2import tkinter as tk 3import tkinter.ttk as ttk 4import threading 5import queue 6import time 7import matplotlib.pyplot as plt 8 9 10####################main###################################################################################################################################### 11 12class MyThread(tk.Frame): 13 threadnum=0 14 15 16 def DrawingValue(self): 17 if self.threadnum==1: 18 self.threadnum=0 19 else: 20 self.threadnum=1 21 22 23 24 def handle_close(self,evt): 25 print("endGUI") 26 self.threadnum=2#グラフを閉じるボタンで消した場合 27 28 29 30 def worker1(self,q): 31 savedata=0#グラフが表示されているか否か 32 33 while 1: 34 if self.threadnum==1: 35 if savedata==0:#グラフが表示されていない場合初期設定を行う 36 savedata=1 37 j=0 38 k=0 39 sinx=[] 40 rangey=list(range(0,1000)) 41 self.fig, self.ax = plt.subplots() 42 self.fig.canvas.draw() 43 bg = self.fig.canvas.copy_from_bbox(self.ax.bbox) 44 line, = self.ax.plot(rangey) 45 self.fig.canvas.mpl_connect('close_event', self.handle_close) 46 self.fig.show() 47 savedata=1 48 while j<1000: 49 sinx.append(0) 50 j+=1 51 52 while self.threadnum==1: 53 if k<1000: 54 k=k+1 55 else: 56 k=0 57 del sinx[0]#先頭を一文字消去 58 sinx.append(k)#末尾に読み出したデータを挿入 59 line.set_ydata(sinx) 60 self.fig.canvas.restore_region(bg) 61 self.ax.draw_artist(line) 62 self.fig.canvas.blit(self.ax.bbox) 63 self.fig.canvas.flush_events() 64 elif self.threadnum==2:#グラフを閉じるボタンで消した場合 65 savedata=0 66 elif self.threadnum==3:#メインを閉じた場合 67 if savedata==1: 68 plt.close() 69 break 70 else: 71 if savedata==1: 72 self.fig.canvas.flush_events()#描画停止時にフリーズを防ぐため 73 time.sleep(0.1) 74 75 76 def __init__(self,master): 77 super().__init__(master, bg='PaleTurquoise1') 78 self.plmi=0 79 80 self.rowconfigure(0, weight=1) 81 self.columnconfigure(0, weight=1) 82 83 # フレームを作成 84 frame_2 = tk.Frame(master=self, relief="groove", bd=3) 85 frame_2.rowconfigure(0, weight=1) 86 frame_2.rowconfigure(1, weight=1) 87 frame_2.columnconfigure(0, weight=1) 88 frame_2.columnconfigure(1, weight=1) 89 90 91 Button3 = tk.Button(frame_2,text=u'描画', command=self.DrawingValue) 92 Button3.grid(row=4, column=0,padx=15, pady=5) 93 frame_2.grid(sticky=tk.NSEW) 94 95 q = queue.Queue() 96 self.t1 = threading.Thread(target=self.worker1, args=(q,), daemon=True) 97 self.t1.start() 98 99 100############################################################################################################## 101 102class Controller(tk.Frame): 103 104 def closingGUI(self): 105 self.Thread1.threadnum=3#※ 106 self.Thread1.t1.join()#※ 107 self.master.destroy() 108 self.master.quit() 109 print("endALL") 110 111 def __init__(self, master): 112 super().__init__(master) 113 self.pack() 114 115 #スレッド1の作成 116 self.Thread1 = MyThread(master=self) 117 self.Thread1.grid(row=0, column=1, sticky=tk.NSEW) 118 119 120 121if __name__ == "__main__": 122 root=tk.Tk() 123 root.title(u"test") 124 app = Controller(master=root) 125 root.protocol("WM_DELETE_WINDOW", app.closingGUI) 126 app.mainloop()

色々試してみたのですが、
スレッドの終了処理(コード内※印)を行わずにデーモンスレッドで終了させたときにはこのエラーが発生しませんでした。
また、描画を行わず即時終了した時もエラーは発生しませんでした。
どこまで削除したらエラーが発生しないかとworker1内を削って確かめた結果、self.fig, self.ax = plt.subplots()を削除した場合エラーが発現しませんでした。

最悪デーモンスレッドで終了でもいいのですが、図を表示したまま終了させたときにエラーが出てしまうので何とかさせたいです、よろしくお願いします。

#####追記####################

追記前よりクローズ処理は実装しておりましたので、GUIの直接操作が問題なのかな?でもGUI直接操作しているかな?と思い、teamikl様の回答をもとに少し触ってみたのですが、「fig, ax = plt.subplots()」や「fig = plt.figure()」をサブスレッドで実行する場合にRuntimeErrorが発生するようでした。print("endworler")も書き出されているのでスレッドは終了しているはずです。

サブスレッドでGUIを動かすべきでないというのはTkinterの話だと思っていたのですが、Matplotlibも実行すべきでないのでしょうか?

元々のプログラムは1ずつ上昇してるグラフの代わりに受信したデータをリアルタイムで表示するようにしているため、メインスレッドはメインループ、サブスレッドはリアルタイム描画用としておりました。
できるだけ早いグラフ更新(2ms更新を目指していたが受信都度更新だと10msが限界だなあと感じていたところ)を目指していたのであまり処理が長くなるのは避けたかったのですが、サブスレッドはデータの受信のみとし、メインに受信データを渡してグラフ描画としなければならないということでしょうか?

別件となりますが、スレッドに描画を任せた理由はリアルタイム処理のためにメインスレッドをループさせるとGUIが操作できないから、というのを忘れてメインスレッドをループさせて描画・更新をさせたのですが
メインスレッドがWhileループ内にいるにもかかわらず、GUIの操作ができ、ボタンも反応しました。(おそらく)self.fig.canvas.flush_events()がループに存在しているのが原因だと思われるのですが、なぜGUIの操作ができるのでしょうか

python

1import tkinter as tk 2import tkinter.ttk as ttk 3from threading import Thread 4import matplotlib.pyplot as plt 5 6 7def worker(root): 8 print("startworler") 9 #fig, ax = plt.subplots()#←これの有無でエラー 10 #fig = plt.figure()#←これの有無でエラー 11 print("endworler") 12 13root = tk.Tk() 14thread = Thread(target=worker, args=(root,)) 15 16root.after(1000*1, thread.start) # 1秒後に開始 17root.after(1000*3, root.quit) # 3秒後にGUIを終了 18root.mainloop()

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

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

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

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

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

guest

回答2

0

サブスレッドでGUIを動かすべきでないというのはTkinterの話だと思っていたのですが、Matplotlibも実行すべきでないのでしょうか?

Matplotlib のバックエンドに依存します。
バックエンドが Tkinter の場合は、plot にも Tkinter が使われます。

できるだけ早いグラフ更新(2ms更新を目指していたが受信都度更新だと10msが限界だなあと感じていたところ)を目指していたのであまり処理が長くなるのは避けたかったのですが、サブスレッドはデータの受信のみとし、メインに受信データを渡してグラフ描画としなければならないということでしょうか?

2ms での更新は、FPS 500 (2ms毎更新 は 1秒間に 500回更新) なので、
殆どの場合は、そこまで細かく更新する必要はないはずです。

目安: 1000ms / 60fps(60Hz) = 16.666.. ms なので

受信はサブスレッドで 2ms 間隔で行うとしても、
メインスレッドでtkinterのタイマー機能を使い
描画反映は、10~15ms間隔で更新すると良いです。

メインスレッドがWhileループ内にいるにもかかわらず、GUIの操作ができ、ボタンも反応しました。(おそらく)self.fig.canvas.flush_events()がループに存在しているのが原因だと思われるのですが、なぜGUIの操作ができるのでしょうか

ここも、Matplotlib のバックエンド次第では挙動が変わる部分ですが、
バックエンドもTkinterの場合は、flush_events により Tkinter の GUI操作等のイベントが処理されます。

バックエンドが異なる場合は、Tkinter の GUI はフリーズするので、
その場合は、root.update() を定期的に呼び出す必要があります。

イベントループイベント処理ループ終了
tkintermainloopupdate/update_idletasksquit
FigureCanvasstart_event_loopflush_eventsstop_event_loop

matplotlib の FigureCanvas では、バックエンドのGUIの対応する処理が、
内部で行われてると考えると理解しやすいと思います。

tkinter のバックエンド matplotlib/backends/_backend_tk.py flush_events

Canvas ウィジェトの update() ですが、
tkinter は全てのウィジェットからアクセスできるようになっているだけなので、
rootとから呼び出す場合と同じものです。
バックエンドが Tkinter の場合は flush_events 内部で update() が呼ばれてます。


質問の最初のコードの

メインスレッドで mainloop
サブスレッドで 独自のループを組み flush_events

という構成について、

任意のタイミングでGUIを更新するのには、flush_events() は有効ですが、
(バックエンドがtkinterの場合は) mainloop との併用は好ましくありません。
mainloop を使う場合は flush_events は極力使わないようにすべきです。

稀にしか起こらないような、再現性の低い不具合を引き起こし、
デバッグが難しくなりやすい傾向があります。

flush_events を使う場合は、独自にイベントループを組みたい場合、
複数のイベントループを組み合わせて使いたい場合等、限られた用途になります。
例えば、

  • Matplotlib のバックエンドがTkinter以外の場合
  • asyncioを一緒に単一スレッド上で使いたい場合、等

今回の場合は、メインスレッドで tkinter のmainloop
サブスレッドで独自に組んだイベントループですが、
バックエンドが同じ tkitner の場合は、イベントループの競合となってしまいます。


追記:

追記前よりクローズ処理は実装しておりましたので、GUIの直接操作が問題なのかな?でもGUI直接操作しているかな?と思い

正確には、別スレッドから直接操作してることにより、
メインスレッド終了後(GUIが破棄された後)に、
破棄済みのリソースに対してのアクセスが発生する点です。

Tkinterは内部でもスレッドを用いてtcl/tk のGUIとやり取りを行っているため、
別スレッドからの操作自体は問題有りませんが、プログラムの構造として、
別スレッドからの操作があると、GUIのライフサイクルとスレッドのライフサイクルが異なり、
質問のエラーとなります。

投稿2021/10/15 11:46

編集2021/10/17 02:17
teamikl

総合スコア8664

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

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

NKJSM

2021/10/17 23:56

追記の補足の回答までありがとうございます。全く気付いていないバグの危険性まで教えていただき本当に助かりました。
guest

0

ベストアンサー

問題点: サブスレッドからのGUIの直接操作

メインスレッド終了後(GUIが破棄された後) に、サブスレッドがGUIにアクセスする事になる為、
問題が起こります。
デーモンスレッドの場合(その他の子スレッドがない場合)、サブスレッドも終了される為
問題が発生しません。

意図的に同じ問題を起こすコード

python

1from threading import Thread 2import tkinter as tk 3 4def worker(root): 5 import time 6 for num in range(5): 7 root.title(f"{num}") 8 time.sleep(1) 9 10root = tk.Tk() 11thread = Thread(target=worker, args=(root,)) 12 13root.after(1000*1, thread.start) # 1秒後に開始 14root.after(1000*3, root.quit) # 3秒後にGUIを終了 15root.mainloop()
  • root.title の部分を print に変更すると、GUIへのアクセスは発生しない為、エラーは起こりません。
  • daemon スレッドにする場合も同様ですが、タイミング次第では起こる可能性は残ります(意図的な再現は難しい)

上で提示したコードの改善例

python

1from threading import Thread, Event 2import tkinter as tk 3 4def worker(root, event): 5 for num, _ in enumerate(range(5), start=1): 6 print(num) 7 if event.is_set(): 8 break 9 root.title(f"{num}") 10 # NOTE: time.sleep では、メインスレッドで thread.join 時に GUIがフリーズする為、 11 # 中断可能な Event.wait を利用。 12 event.wait(1) 13 14root = tk.Tk() 15event = Event() 16event.clear() 17thread = Thread(target=worker, args=(root, event)) 18 19def on_close(): 20 event.set() # 修了を通知 21 thread.join() # workerの完了を待つ 22 root.destroy() # GUIの破棄 (ウィンドウを閉じる) 23root.protocol("WM_DELETE_WINDOW", on_close) 24 25root.after(1000*1, thread.start) 26root.after(1000*3, on_close) 27root.mainloop()

暫定的な回避策

python

1root.protocol("WM_DELETE_WINDOW", func)

で、閉じるボタンを押した時・終了前に関数を実行できるので、
その関数の中でサブスレッドを安全に終了できるようにしてください。

サブスレッドで実行される関数では、while 1: と無限ループになってますが、
ここを変数にして、終了前にループを抜けられるように変更します。

関連: Teratail - Tcl_AsyncDelete: async handler deleted by the wrong thread 対策


根本的な解決策

RuntimeError: main thread is not in main loop

というエラーは、サブスレッドでGUIを直接操作している際によく起こります。

メインスレッドは終了してGUIのリソースは破棄されているが、
サブスレッドは稼働していて、破棄されたはずのGUIのリソースを参照しようとしているような状態です。

サブスレッドでGUIのコードを混在するようなプログラムは、
デバッグが困難になる傾向がある為、
メインスレッド(GUI) とサブスレッドを完全に分離するような、
スレッドセーフな設計をお勧めします。

コードは大幅な変更を強いる為、概要のみの提示ですが

  • サブスレッドでは GUI に関連のない演算のみを行う。
  • サブスレッドの演算結果をGUIに反映させたい場合は、キューで通知。

 (簡易的には tkinter の場合はキューの代わりに after_idle も利用可)

  • メインスレッドでは、タイマーを用いて定期的にキューから読み出す。

 (tkinterの場合は after 関数を使い定期的に実行)

  • ※ 現在のコードは、Queue を引数に渡していますが、活用されてません。

投稿2021/10/14 11:48

編集2021/10/14 12:13
teamikl

総合スコア8664

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

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

NKJSM

2021/10/15 08:04

回答ありがとうございました。 長くなるので本文の方に書かせてもらいましたが、意見をお聞きして色々試して、メインスレッドでリアルタイム処理しようとするとQueue待ち等でmainloopの監視が難しいなということでafterでその間に受信したデータをまとめて更新の形にしようと思いました。 疑問点はまだいくつか残りますが、最初の疑問は解消いたしましたのでベストアンサーとさせていただき、締めとさせていただきます。
teamikl

2021/10/15 11:15

本文確認しましたが、長くなりそうなので別回答にて返信します。 > メインスレッドでリアルタイム処理しようとするとQueue待ち等でmainloopの監視が難しいなということで > afterでその間に受信したデータをまとめて更新の形にしようと思いました。 変更範囲が広くなるので、回答では概要のみの提示としましたが、 可能ならその方法での実装が良いと思います。 Queue の待ちに関しては、いくつか方法がありますが メインスレッドでイベントループを阻害しないようにする為には、 ノンブロッキングの get_nowait での読み出しにします。
guest

あなたの回答

tips

太字

斜体

打ち消し線

見出し

引用テキストの挿入

コードの挿入

リンクの挿入

リストの挿入

番号リストの挿入

表の挿入

水平線の挿入

プレビュー

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

ただいまの回答率
85.48%

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

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

質問する

関連した質問