57547


スレッド間通信のためにQueueを追加し、Queueの受信ループを追加したところ、通信相手のスレッドが停止（SleepあるいはSuspend)してしまったかのような状態になりました。受信内容を表示させるための、tk.Textウィジットへの処理で停止していることが分かりました。

＜知りたいこと＞  
デバッグ方法として、どのように切り分けていけば良いかご教示をお願いします。  
tk.Textウィジットへ出力とQueueの受信ループを共存させるにはどのようにしたらよいかご教示をお願いします。

＜具体的な事象＞  
以下に示す簡略化コードと対比して説明します。  
サブスレッドA：通信スレッドｰ>class_Communicaiotn_subthreadに相当  
サブスレッドB：GUI用スレッドｰ>class_MainScreen_subthreadに相当  
メインスレッド：GUIスレッドｰ>class_MainScreenInit内self.MainWindow.mainloop()に相当  
GUI定期処理：外部割込みに応じたGUIへの処理 class_MainScreenInit内MainWindow_Com関数に相当  
上記のような処理が独立して動作する状態です。

動作の目的：メインスレッドから外部通信により機器の状態を取得する。  
１．メインスレッドからQueueを介して、サブスレッドAへコマンド送信を依頼。  
２．サブスレッドAは外部通信を行い、データ取得通知（外部通信側にバッファする機構があるため）を受信する。  
３．サブスレッドAはデータ取得通知をもとに、データを取得する。（データ取得通知が終わるまで）  
４．サブスレッドAは取得データをある程度バッファして整形し、サブスレッドBへQueueを介して引き渡す。  
５．サブスレッドBは、処理１に必要なデータか判断して、必要な場合、Queueを介してメインスレッドへ引き渡す。かつtk.Textへデータを出力する。  
６．メインスレッドは、サブスレッドBが挿入するQueueを受信ループで待つ。  
このような流れですが、処理６に入ると、サブスレッドBがtk.Textへデータを出力部分で停止して、永久にメインスレッドにデータが渡りません。メインスレッド（空回り状態）、サブスレッドAは動作しています。

処理６をスキップすると、処理５は正常にtk.Textへデータを出力します。

元のソースコードは複雑なので、マルチスレッド処理部分を簡略化して記述しましたが。簡略化したコードは以下です。

[code]

    import time
    import traceback
    
    import threading
    from queue import Empty, Queue
    
    import tkinter as tk
    
    def_queue = {'com_send':0,'com_recv':1}
    
    class class_MainScreenCom(object):
        def __init__(self, queue_list:list()):
            self.queue_list = queue_list
            self.share_obj = list()
            return
    
        def run(self):
            gui_sub_thread = class_MainScreen_subthread(self.queue_list)
            gui_sub_thread.share(self.share_obj)
            gui_sub_thread.start()
    
            gui_obj = class_MainScreenInit(self.queue_list)
            gui_obj.share(self.share_obj)
            gui_obj.run()
    
            gui_sub_thread.join()
            return
    
    class class_MainScreenInit(object):
        def __init__(self, queue_list:list()):
            self.cmd_count = 0
    
            self.hQueue_Com_send = queue_list[def_queue['com_send']]
            self.hQueue_Com_recv = queue_list[def_queue['com_recv']]
    
            self.hQueue_GUI = Queue()
    
            self.MainWindow = tk.Tk()
            geo_string = "400x200+0+0"
            self.MainWindow.geometry(geo_string)
    
            _InFrame_ = tk.Frame(
                self.MainWindow,
                )
    
            self.btnSendCmd = tk.Button(
                _InFrame_,
                text = 'send',
                command = self.send_clicked,
                )
    
            self.txtMsg = tk.Text(
                _InFrame_,
                )
    
    
            _InFrame_.pack()
            self.btnSendCmd.pack()
            self.txtMsg.pack()
    
            self.MainWindow.bind('<Destroy>',self._quit)
    
            return
    
        def _quit(self, event):
            return
    
        def share(self, share_obj):
            share_obj.append(self)
    
        def run(self):
            self.MainWindow.after(100,self.MainWindow_Com)
            self.MainWindow.mainloop()
    
            return
    
        def send_clicked(self):
            recv_try_interval = 0.1
            recv_data = list()
    
            for i in range(8):
                self.hQueue_Com_recv.put(self.cmd_count)
                self.cmd_count += 1
                time.sleep(recv_try_interval*2)
    
            #return #ここでreturnするとTextに正常に表示できる。
    
            while len(recv_data) < 8:
                time.sleep(recv_try_interval)
    
                while not self.hQueue_GUI.empty():
                    try:
                        item = self.hQueue_GUI.get(block=False)
                        print('[GUI]hQueue_GUI:{0}'.format(item))
                        if isinstance(item,dict):
                            recv_data.append(item)
                    except:
                        traceback.print_exc()
    
            return
    
        def MainWindow_Com(self):
            self.MainWindow.after(100,self.MainWindow_Com)
            return
    
    
    class class_MainScreen_subthread(threading.Thread):
        def __init__(self, queue_list:list(), group = None, target = None, name = None, args = (), kwargs = None, daemon = None):
            self.loop_flg = True
            self.hQueue_Com_send = queue_list[def_queue['com_send']]
            self.hQueue_Com_recv = queue_list[def_queue['com_recv']]
    
            return super().__init__()
    
        def join(self, timeout = None):
            self.loop_flg = False
            return super().join(timeout)
    
        def share(self, share_obj):
            self.share_obj = share_obj
    
        def run(self):
            recv_try_interval = 0.1
            ret_param = None
            cmd_set = dict()
    
            while(self.loop_flg):
                time.sleep(recv_try_interval)
    
                while not self.hQueue_Com_send.empty():
                    try:
                        item = self.hQueue_Com_send.get(block=False)
                        print('[GUI]hQueue_Com_send:{0}'.format(item))
                        if isinstance(item,int):
                            ret_param = item + 1
                    except:
                        traceback.print_exc()
    
                if ret_param != None:
                    ntime = time.time()
                    if ret_param in cmd_set:
                        cmd_set[ret_param] = ntime
                    else:
                        cmd_set.setdefault(ret_param, ntime)
                    ret_param = None
    
                if len(cmd_set) >= 8:
                    for cmd in cmd_set:
                        set_item = {cmd:cmd_set[cmd]}
                        self.share_obj[0].txtMsg.insert(tk.END,set_item)
                        self.share_obj[0].hQueue_GUI.put(set_item)
                    else:
                        cmd_set.clear()
    
    class class_Communicaiotn_subthread(threading.Thread):
        def __init__(self, queue_list:list(), group = None, target = None, name = None, args = (), kwargs = None, daemon = None):
            self.loop_flg = True
            self.hQueue_Com_send = queue_list[def_queue['com_send']]
            self.hQueue_Com_recv = queue_list[def_queue['com_recv']]
    
            return super().__init__()
    
        def join(self, timeout = None):
            self.loop_flg = False
            return super().join(timeout)
    
        def run(self):
            recv_try_interval = 0.1
            ret_param = None
    
            while(self.loop_flg):
                time.sleep(recv_try_interval)
    
                while not self.hQueue_Com_recv.empty():
                    try:
                        item = self.hQueue_Com_recv.get(block=False)
                        print('[COM]hQueue_Com_recv:{0}'.format(item))
    
                        if isinstance(item,int):
                            ret_param = item + 1
                            self.hQueue_Com_recv.put('get send_cmd')
                            time.sleep(recv_try_interval)
                        elif isinstance(item,str):
                            if item == 'get send_cmd':
                                self.hQueue_Com_send.put(ret_param)
                    except:
                        traceback.print_exc()
    
            return super().run()    
    
    class class_AppStart():
        def __init__(self):
            self.hQueue_ComSend = Queue()
            self.hQueue_ComRecv = Queue()
            self.hQueue_ScreenSend = Queue()
            self.hQueue_ScreenRecv = Queue()
            return
    
        def run(self):
            queue_list = [self.hQueue_ComSend,self.hQueue_ComRecv,self.hQueue_ScreenSend,self.hQueue_ScreenRecv]
            threadCom = class_Communicaiotn_subthread(queue_list)
            threadCom.start()
    
            gui_obj = class_MainScreenCom(queue_list)
            gui_obj.run()
    
            threadCom.join()
    
            return
    
    
    if __name__ == '__main__':
        app_obj = class_AppStart()
        app_obj.run()
    
    
[/code]


CC BY-SA 4.0
2019-08-22T06:27:34.033
57534
2019-08-22T13:01:16.360
2019-08-22T08:36:29.820
3060
32891
question
python python3 tk
Python　マルチスレッド処理のデバッグ方法→tk.Textウィジットでの処理停止について


実際の現象では時系列的に「処理5」よりも「処理6」の方が先に実行されています。  
そして実際の「処理5」の中は、「tk.Textへデータを出力する」が先で「Queueを介してメインスレッドへ引き渡す」が後でした。

調査方法としては泥臭い`print()`挿入でやってみました。  
対処も含めて以下の様になります。

  1. 問題となっていそうなメインスレッドの`def send_clicked(self):`の`while len(recv_data) < 8:`ループ内の適当な箇所に`print()`を仕掛ける → 以下が判明

    * 初期値が0の`recv_data`が8以上になったらループ終了のはず
    * `recv_data`のカウントアップは`self.hQueue_GUI`キューからのデータ取得
    * ただし`self.hQueue_GUI`キューは空のまま無限ループ状態
    * 誰が何処で`hQueue_GUI`へのキューイングしているかエディタのgrepで検索
    * `class class_MainScreen_subthread(threading.Thread):`の`def run(self):`の`self.share_obj[0].hQueue_GUI.put(set_item)`でキューイングしている
  2. サブスレッドの上記処理の前後に`print()`を仕掛ける → 以下が判明

    * キューイングの直前の`self.share_obj[0].txtMsg.insert(tk.END,set_item)`の処理から戻っていないためキューイングが呼ばれていない
    * 上記Textウィジェットへの挿入だけコメントアウトすると、テキストは表示されないが、処理は停止しない
    * おそらくメインスレッドが「send」ボタン処理内で実質的無限ループしている最中に、メインスレッドの担当するUI部品(Textウィジェット)をサブスレッドから更新しようとしたためにデッドロック状態が発生と判断
  3. 対処としては、Queue受信ループを「send」ボタン処理から外して他に移動させる

    * メインスレッド内で既に定期的に呼ばれる処理はないか確認(無ければ新規に追加)
    * `def send_clicked(self):`直後の`def MainWindow_Com(self):`が該当
    * `def send_clicked(self):`のQueue受信ループを(そのままではなく相当の処理にして)`def MainWindow_Com(self):`に移動
  4. tk.Textウィジットへ出力とQueueの受信ループを共存させる変更の詳細は以下

    * `def send_clicked(self):`ではQueue受信ループの前に`return`で終了
    * `recv_data = list()`を`def send_clicked(self):`から`def __init__(self, queue_list:list()):`に移動して頭に`self.`を追加
    * Queue受信ループの内側のループだけを`def MainWindow_Com(self):`に移動
    * `recv_data.append(item)`の頭に`self.`を追加

`def MainWindow_Com(self):`の処理は以下になります。

[code]

    def MainWindow_Com(self):
        while not self.hQueue_GUI.empty():
            try:
                item = self.hQueue_GUI.get(block=False)
                print('[GUI]hQueue_GUI:{0}'.format(item))
                if isinstance(item,dict):
                    self.recv_data.append(item)  #### self. を追加
            except:
                traceback.print_exc()
    
        self.MainWindow.after(100,self.MainWindow_Com)
        return
    
[/code]


CC BY-SA 4.0
2019-08-22T13:01:16.360
57547
2019-08-22T13:01:16.360
26370
57534
answer