无法理解为什么在Windows下的(wx)Python中,停止线程通常会挂起该线程的剩余执行代码
我有一个wxPython代码,在Windows 10 64位/Python 3.9.0 64位/wx'4.1.1 msw(phoenix)wxWidgets 3.1.5'下运行,它在外部文件中启动一个线程(如果这对我的问题有任何影响的话)。真实代码启动telnet会话,但为了简单(和理解),我创建了一个单独的工作测试程序,如下所示,它遵循与真实代码相同的逻辑,只是它删除了telnet部分 该程序有一个“连接”工具栏按钮,用于启动状态栏上显示报告消息的线程;还有一个“断开”按钮,用于正常停止线程(至少我是这么想的),状态栏上显示报告消息 单击工具栏上的“连接”按钮(即“+”按钮),线程开始正常运行 我的问题是:只要我单击工具栏上的“断开连接”按钮(即“-”按钮),程序就会挂起,这可能是因为执行了无休止的线程。这就像无法理解为什么在Windows下的(wx)Python中,停止线程通常会挂起该线程的剩余执行代码,python,multithreading,wxpython,Python,Multithreading,Wxpython,我有一个wxPython代码,在Windows 10 64位/Python 3.9.0 64位/wx'4.1.1 msw(phoenix)wxWidgets 3.1.5'下运行,它在外部文件中启动一个线程(如果这对我的问题有任何影响的话)。真实代码启动telnet会话,但为了简单(和理解),我创建了一个单独的工作测试程序,如下所示,它遵循与真实代码相同的逻辑,只是它删除了telnet部分 该程序有一个“连接”工具栏按钮,用于启动状态栏上显示报告消息的线程;还有一个“断开”按钮,用于正常停止线程(
stopped
函数冻结一切,而不仅仅是让while…
循环离开并简单地沿着它的路径离开
通过在非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警告和随机Gdk错误,很可能是由于我简化的wx测试代码的一些缺陷,但我只是暂时忽略了这一点)
也许这不是Windows特有的问题,也许我的程序的逻辑有一些缺陷
我错过了什么
注意:为了简化此测试,程序窗口的close按钮不会在程序退出之前尝试结束线程(如果已启动),并且“Disconnect”按钮事件不会检查是否确实有东西要断开
稍后编辑:我在相同的窗口下测试了它,也使用了Python 3.9.1 32位/wx'4.1.1 msw(phoenix)wxWidgets 3.1.5'(这一个是使用32位Python安装的pip安装的):没有区别,程序以相同的方式挂起
主要代码:
#!/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, *args, **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, "Connect", "")
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, wx.ART_TOOLBAR, (24, 24)), wx.NullBitmap, wx.ITEM_NORMAL, "Disconnect", "")
self.Bind(wx.EVT_TOOL, 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)
和外部文件上的代码(实际线程):
测试窗口(Windows下)如下所示(单击工具栏上的“连接”按钮后显示):
首先必须承认这是不直观的。试图省略
self.ctrl\u 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")
我想您不相信线程会自动退出,因为您最终会杀死所有东西:)
简而言之,在wxPython代码中,必须永远不要有阻塞的代码,因为这将阻止任何进一步的代码被处理。输入线程
在您的情况下,wx事件循环进入您的方法
ctrl\u disconnect
。join()
表示它正在等待线程停止。但是,线程中的代码试图让wxPython返回ecececute代码(self.wx\u ui.frame\u statusbar.SetStatusText
),这无法继续,因为wxPython循环仍在等待join()
完成。答案在wxPython代码中的语句中,您永远不能有阻塞的代码,因为它将阻止任何进一步的代码被处理。事实上,问题在于.join()
事实(而不是方法),因为我还尝试了其他一些不同的等待线程关闭的方法,这些方法实际上以相同的方式锁定了程序。事实证明,移动特定于UI的状态栏更新并不重要(我以前尝试过完全删除它),而且也不是一个解决方案,因为在我的实际代码中,线程必须在那里写入其他几条消息。总之,谢谢:)(继续上面的评论,因为它太长了)但是,我不明白为什么这个测试在Raspberry Pi(即Linux)上“按原样”工作;这完全把我弄糊涂了。别担心:在Debian 64位Buster(apt install python3-wxgtk4.0
)上尝试了问题中发布的代码。它会在按下connect时崩溃。在线程对wxPython的第一次调用中也必须移出。我认为raspbian wxPython已经足够多了,它可以让您避免线程错误处理:)另一个注意事项:wxPython不是线程安全的。因此,建议从非wx线程使用wx.CallAfter(线程安全)。如下所示:wx.CallAfter(self.wx\u ui.frame\u statusbar.SetStatusText,“断开”)
。您可能已经猜到了:这将在Debian上起作用,但在Windows上不起作用:/I按照上面的建议测试了wx.CallAfter(self.etc.)
,它在Windows和Raspberry Pi OS上都能很好地工作:)这与公认答案的主要解决方案(即避免/删除任何等待/阻塞代码)相结合,产生了一个更强大的工作程序。谢谢:)
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)