Python マルチスレッド処理のデバッグ方法→tk.Textウィジットでの処理停止について
スレッド間通信のために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関数に相当
上記のような処理が独立して動作する状態です。
動作の目的:メインスレッドから外部通信により機器の状態を取得する。
1.メインスレッドからQueueを介して、サブスレッドAへコマンド送信を依頼。
2.サブスレッドAは外部通信を行い、データ取得通知(外部通信側にバッファする機構があるため)を受信する。
3.サブスレッドAはデータ取得通知をもとに、データを取得する。(データ取得通知が終わるまで)
4.サブスレッドAは取得データをある程度バッファして整形し、サブスレッドBへQueueを介して引き渡す。
5.サブスレッドBは、処理1に必要なデータか判断して、必要な場合、Queueを介してメインスレッドへ引き渡す。かつtk.Textへデータを出力する。
6.メインスレッドは、サブスレッドBが挿入するQueueを受信ループで待つ。
このような流れですが、処理6に入ると、サブスレッドBがtk.Textへデータを出力部分で停止して、永久にメインスレッドにデータが渡りません。メインスレッド(空回り状態)、サブスレッドAは動作しています。
処理6をスキップすると、処理5は正常にtk.Textへデータを出力します。
元のソースコードは複雑なので、マルチスレッド処理部分を簡略化して記述しましたが。簡略化したコードは以下です。
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()