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

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

ただいまの
回答率

87.51%

threadでmatplotlib時のエラー対処法

解決済

回答 2

投稿 編集

  • 評価
  • クリップ 1
  • VIEW 70

score 8

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
と言われてしまいます。
これの対処法は何かございますでしょうか?

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

    # ライブラリ
import tkinter as tk
import tkinter.ttk as ttk
import threading
import queue
import time
import matplotlib.pyplot as plt


####################main######################################################################################################################################

class MyThread(tk.Frame):
    threadnum=0


    def DrawingValue(self):
        if self.threadnum==1:
            self.threadnum=0
        else:
            self.threadnum=1



    def handle_close(self,evt):
        print("endGUI")
        self.threadnum=2#グラフを閉じるボタンで消した場合



    def worker1(self,q):
        savedata=0#グラフが表示されているか否か

        while 1:
            if self.threadnum==1:
                if savedata==0:#グラフが表示されていない場合初期設定を行う        
                    savedata=1    
                    j=0
                    k=0
                    sinx=[]
                    rangey=list(range(0,1000))
                    self.fig, self.ax = plt.subplots()
                    self.fig.canvas.draw()
                    bg = self.fig.canvas.copy_from_bbox(self.ax.bbox)
                    line, = self.ax.plot(rangey)
                    self.fig.canvas.mpl_connect('close_event', self.handle_close)
                    self.fig.show()
                    savedata=1        
                    while j<1000:
                        sinx.append(0)
                        j+=1

                while self.threadnum==1:
                    if k<1000:    
                        k=k+1
                    else:
                        k=0    
                    del sinx[0]#先頭を一文字消去
                    sinx.append(k)#末尾に読み出したデータを挿入        
                    line.set_ydata(sinx)
                    self.fig.canvas.restore_region(bg)
                    self.ax.draw_artist(line)
                    self.fig.canvas.blit(self.ax.bbox)
                    self.fig.canvas.flush_events()        
            elif self.threadnum==2:#グラフを閉じるボタンで消した場合
                savedata=0
            elif self.threadnum==3:#メインを閉じた場合
                if savedata==1:
                    plt.close()
                break
            else:
                if savedata==1:
                    self.fig.canvas.flush_events()#描画停止時にフリーズを防ぐため
                time.sleep(0.1)


    def __init__(self,master):
        super().__init__(master, bg='PaleTurquoise1')
        self.plmi=0

        self.rowconfigure(0, weight=1)
        self.columnconfigure(0, weight=1)

        # フレームを作成
        frame_2 = tk.Frame(master=self, relief="groove", bd=3)
        frame_2.rowconfigure(0, weight=1)
        frame_2.rowconfigure(1, weight=1)
        frame_2.columnconfigure(0, weight=1)
        frame_2.columnconfigure(1, weight=1)


        Button3 = tk.Button(frame_2,text=u'描画', command=self.DrawingValue)
        Button3.grid(row=4, column=0,padx=15, pady=5)
        frame_2.grid(sticky=tk.NSEW)

        q = queue.Queue()
        self.t1 = threading.Thread(target=self.worker1, args=(q,), daemon=True)
        self.t1.start()


##############################################################################################################

class Controller(tk.Frame):

    def closingGUI(self):
        self.Thread1.threadnum=3#※
        self.Thread1.t1.join()#※    
        self.master.destroy()
        self.master.quit()
        print("endALL")

    def __init__(self, master):
        super().__init__(master)
        self.pack()

        #スレッド1の作成
        self.Thread1 = MyThread(master=self)
        self.Thread1.grid(row=0, column=1, sticky=tk.NSEW)



if __name__ == "__main__":
    root=tk.Tk()
    root.title(u"test")
    app = Controller(master=root)
    root.protocol("WM_DELETE_WINDOW", app.closingGUI)
    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の操作ができるのでしょうか

import tkinter as tk
import tkinter.ttk as ttk
from threading import Thread
import matplotlib.pyplot as plt


def worker(root):
    print("startworler")
    #fig, ax = plt.subplots()#←これの有無でエラー
    #fig = plt.figure()#←これの有無でエラー
    print("endworler")

root = tk.Tk()
thread = Thread(target=worker, args=(root,))

root.after(1000*1, thread.start) # 1秒後に開始
root.after(1000*3, root.quit) # 3秒後にGUIを終了
root.mainloop()
  • 気になる質問をクリップする

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

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

    クリップを取り消します

  • 良い質問の評価を上げる

    以下のような質問は評価を上げましょう

    • 質問内容が明確
    • 自分も答えを知りたい
    • 質問者以外のユーザにも役立つ

    評価が高い質問は、TOPページの「注目」タブのフィードに表示されやすくなります。

    質問の評価を上げたことを取り消します

  • 評価を下げられる数の上限に達しました

    評価を下げることができません

    • 1日5回まで評価を下げられます
    • 1日に1ユーザに対して2回まで評価を下げられます

    質問の評価を下げる

    teratailでは下記のような質問を「具体的に困っていることがない質問」、「サイトポリシーに違反する質問」と定義し、推奨していません。

    • プログラミングに関係のない質問
    • やってほしいことだけを記載した丸投げの質問
    • 問題・課題が含まれていない質問
    • 意図的に内容が抹消された質問
    • 過去に投稿した質問と同じ内容の質問
    • 広告と受け取られるような投稿

    評価が下がると、TOPページの「アクティブ」「注目」タブのフィードに表示されにくくなります。

    質問の評価を下げたことを取り消します

    この機能は開放されていません

    評価を下げる条件を満たしてません

    評価を下げる理由を選択してください

    詳細な説明はこちら

    上記に当てはまらず、質問内容が明確になっていない質問には「情報の追加・修正依頼」機能からコメントをしてください。

    質問の評価を下げる機能の利用条件

    この機能を利用するためには、以下の事項を行う必要があります。

回答 2

checkベストアンサー

+1

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

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

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

from threading import Thread
import tkinter as tk

def worker(root):
    import time
    for num in range(5):
        root.title(f"{num}")
        time.sleep(1)

root = tk.Tk()
thread = Thread(target=worker, args=(root,))

root.after(1000*1, thread.start) # 1秒後に開始
root.after(1000*3, root.quit) # 3秒後にGUIを終了
root.mainloop()
  • root.title の部分を print に変更すると、GUIへのアクセスは発生しない為、エラーは起こりません。
  • daemon スレッドにする場合も同様ですが、タイミング次第では起こる可能性は残ります(意図的な再現は難しい)

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

from threading import Thread, Event
import tkinter as tk

def worker(root, event):
    for num, _ in enumerate(range(5), start=1):
        print(num)
        if event.is_set():
            break
        root.title(f"{num}")
        # NOTE: time.sleep では、メインスレッドで thread.join 時に GUIがフリーズする為、
        # 中断可能な Event.wait を利用。
        event.wait(1)

root = tk.Tk()
event = Event()
event.clear()
thread = Thread(target=worker, args=(root, event))

def on_close():
    event.set() # 修了を通知
    thread.join() # workerの完了を待つ
    root.destroy() # GUIの破棄 (ウィンドウを閉じる)
root.protocol("WM_DELETE_WINDOW", on_close)

root.after(1000*1, thread.start)
root.after(1000*3, on_close)
root.mainloop()

暫定的な回避策

root.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/15 17:04

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

    キャンセル

  • 2021/10/15 20:15

    本文確認しましたが、長くなりそうなので別回答にて返信します。

    > メインスレッドでリアルタイム処理しようとするとQueue待ち等でmainloopの監視が難しいなということで
    > afterでその間に受信したデータをまとめて更新の形にしようと思いました。

    変更範囲が広くなるので、回答では概要のみの提示としましたが、
    可能ならその方法での実装が良いと思います。

    Queue の待ちに関しては、いくつか方法がありますが
    メインスレッドでイベントループを阻害しないようにする為には、
    ノンブロッキングの get_nowait での読み出しにします。

    キャンセル

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() を定期的に呼び出す必要があります。

   イベントループ   イベント処理   ループ終了 
 tkinter   mainloop   update/update_idletasks   quit 
 FigureCanvas   start_event_loop   flush_events   stop_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 の場合は、イベントループの競合となってしまいます。

投稿

  • 回答の評価を上げる

    以下のような回答は評価を上げましょう

    • 正しい回答
    • わかりやすい回答
    • ためになる回答

    評価が高い回答ほどページの上位に表示されます。

  • 回答の評価を下げる

    下記のような回答は推奨されていません。

    • 間違っている回答
    • 質問の回答になっていない投稿
    • スパムや攻撃的な表現を用いた投稿

    評価を下げる際はその理由を明確に伝え、適切な回答に修正してもらいましょう。

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

  • ただいまの回答率 87.51%
  • 質問をまとめることで、思考を整理して素早く解決
  • テンプレート機能で、簡単に質問をまとめられる

関連した質問

同じタグがついた質問を見る