问题描述
我有一个 wxPython 代码,在 Windows 10 64bit / Python 3.9.0 64bit / wx '4.1.1 msw (phoenix) wxWidgets 3.1.5' 下运行,它在外部文件中启动一个线程(如果这对任何我的问题的方法)。真实代码启动一个 telnet 会话,但为了简单(和理解)我创建了一个单独的工作测试程序,如下所示,它遵循与真实代码相同的逻辑,只是它删除了 telnet 部分。
该程序有一个“连接”工具栏按钮,可以在状态栏上启动一个带有报告消息的线程,还有一个“断开连接”按钮可以正常停止该线程(嗯,至少这是我的意图)再次在状态栏上显示报告消息状态栏。
通过单击工具栏上的“连接”按钮(即“+”按钮),线程开始正常。
我的问题:就我单击工具栏上的“断开连接”按钮(即“-”按钮)而言,程序挂起,据说是因为无限线程执行。这就像 stopped
函数冻结了所有内容,而不仅仅是让 while ...
循环离开并简单地跟随它的出路。
通过将线程的循环 while not self.stopped():
语句更改为 while True
后跟由几秒钟超时触发的 break
(因此,无需进一步触摸“断开连接”按钮),线程在超时后正常退出,就好像什么也没发生一样——所以问题出在实际的线程停止机制上。
然而,在 RaspBerry Pi OS (Buster) / Python 3.7.3 / wx '4.0.7 post2 gtk3 (phoenix) wxWidgets 3.0.5' 下运行相同的测试程序时,不再发生挂起(我得到了一些Gtk-WARNING 和随机 Gdk-ERROR,很可能是由于我简化的 wx 测试代码存在一些缺陷,但我暂时忽略了这一点)。
也许这不是 Windows 特有的问题,也许是我的程序逻辑存在缺陷。
我在这里想念什么?
注意:为了这个测试的简单性,程序窗口的关闭按钮不会在程序退出之前尝试结束线程(如果已启动),并且“断开连接”按钮事件不会检查是否真的有东西要断开连接。
稍后编辑:我在相同的 Windows 下测试了它,也使用 Python 3.9.1 32bit / wx '4.1.1 msw (phoenix) wxWidgets 3.1.5'(这个使用 pip 安装32 位 Python 安装):没有区别,程序以相同方式挂起。
主要代码:
#!/usr/bin/env python3
# -*- coding: UTF-8 -*-
import test_wx_th_ext_file as TestWxThExtFile
import wx
import threading
import time
import os
import sys
##
class MyFrame(wx.Frame):
def __init__(self,*args,**kwds):
kwds["style"] = kwds.get("style",0) | wx.DEFAULT_FRAME_STYLE
wx.Frame.__init__(self,**kwds)
self.SetSize((400,300))
self.SetTitle("test wx th")
self.frame_statusbar = self.CreateStatusBar(1)
self.frame_statusbar.SetStatusWidths([-1])
# Tool Bar
self.frame_toolbar = wx.ToolBar(self,-1)
tool = self.frame_toolbar.AddTool(wx.ID_ANY,"Connect",wx.ArtProvider.GetBitmap(wx.ART_PLUS,wx.ART_TOOLBAR,(24,24)),wx.NullBitmap,wx.ITEM_norMAL,"")
self.Bind(wx.EVT_TOOL,self.ctrl_connect,id=tool.GetId())
tool = self.frame_toolbar.AddTool(wx.ID_ANY,"disconnect",wx.ArtProvider.GetBitmap(wx.ART_MINUS,self.ctrl_disconnect,id=tool.GetId())
self.SetToolBar(self.frame_toolbar)
self.frame_toolbar.Realize()
# Tool Bar end
self.panel_1 = wx.Panel(self,wx.ID_ANY)
self.Layout()
def ctrl_connect(self,event):
self.ctrl_thread = TestWxThExtFile.ControllerTn(self)
self.ctrl_thread.daemon = True
self.ctrl_thread.start()
def ctrl_disconnect(self,event):
self.ctrl_thread.stopit()
self.ctrl_thread.join()
##
class MyApp(wx.App):
def OnInit(self):
self.frame = MyFrame(None,wx.ID_ANY,"")
self.SetTopWindow(self.frame)
self.frame.Show()
return True
##
if __name__ == "__main__":
app = MyApp(0)
app.MainLoop()
try:
sys.exit(0)
except SystemExit:
os._exit(0)
#!/usr/bin/env python
# -*- coding: utf-8 -*-
import threading
import time
##
class ControllerTh(threading.Thread):
def __init__(self):
super().__init__()
self._stopper = threading.Event()
def stopit(self):
self._stopper.set()
def stopped(self):
return self._stopper.is_set()
##
class ControllerTn(ControllerTh):
def __init__(self,wx_ui):
ControllerTh.__init__(self)
self.wx_ui = wx_ui
def run(self):
print ("th_enter")
self.wx_ui.frame_statusbar.SetStatusText("Connected")
while not self.stopped():
print ("th_loop")
time.sleep(1)
self.wx_ui.frame_statusbar.SetStatusText("disconnected")
print ("th_exit")
测试窗口(在 Windows 下)看起来像这样(点击工具栏上的“连接”按钮后显示在这里):
解决方法
不得不承认这起初是不直观的。试图省略 self.ctrl_thread.join()
。这是您问题的真正原因,您需要考虑另一种安全停止线程的方法。
要使其运行,请将特定于 UI 的状态栏更新从线程移回框架。问题解决了。在线程中:
while not self.stopped():
print ("th_loop")
time.sleep(1)
# move this to the frame
# self.wx_ui.frame_statusbar.SetStatusText("Disconnected")
print ("th_exit")
在框架中:
def ctrl_disconnect(self,event):
self.ctrl_thread.stopit()
self.ctrl_thread.join()
# thread has joined,signal end of thread
self.frame_statusbar.SetStatusText("Disconnected")
我想您不相信线程会自行退出,因为无论如何您最终都会杀死所有内容:)
# hooray,all threads will be dead :)
try:
sys.exit(0)
except SystemExit:
os._exit(0)
简而言之,在 wxPython 代码中,你必须永远不要有代码阻塞,因为它会阻止任何进一步的代码被处理。输入话题。
在您的情况下,wx 事件循环进入您的方法 ctrl_disconnect
。 join()
表示它正在等待线程停止。但是,您在线程中的代码试图让 wxPython 执行代码 (self.wx_ui.frame_statusbar.SetStatusText
),这无法继续,因为 wxPython 循环仍在等待 join()
完成。