无法理解为什么正常停止线程会在 Windows 下的 (wx)Python 中挂起该线程的剩余执行代码
Cannot understand why stopping a thread normally will hang the remaining execution code from that thread in (wx)Python under Windows
我有wxPython代码,运行下Windows10 64bit/Python3.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 PiOS(Buster)/Python3.7.3/wx'4.0.7 post2 gtk3(phoenix ) wxWidgets 3.0.5',挂起不再发生(我得到了一些 Gtk-WARNING 和随机 Gdk-ERROR,很可能是由于我简化的 wx 测试代码的一些不完善,但我现在只是忽略了它)。
也许这不是 Windows 特有的问题,也许我的程序逻辑有一些缺陷。
我在这里想念什么?
注意:为简化此测试,程序的关闭按钮 window 不会尝试在程序退出之前结束线程(如果已启动),并且不会检查“断开连接”按钮事件如果真的有东西要断开连接。
稍后编辑:我测试了它,在相同的Windows下,也用Python 3.9.1 32bit / wx '4.1.1 msw (phoenix ) wxWidgets 3.1.5'(这个是用32位的pip安装的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, *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)
和外部文件(实际线程)上的代码:
#!/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")
测试 window(在 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()
完成。
我有wxPython代码,运行下Windows10 64bit/Python3.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 PiOS(Buster)/Python3.7.3/wx'4.0.7 post2 gtk3(phoenix ) wxWidgets 3.0.5',挂起不再发生(我得到了一些 Gtk-WARNING 和随机 Gdk-ERROR,很可能是由于我简化的 wx 测试代码的一些不完善,但我现在只是忽略了它)。
也许这不是 Windows 特有的问题,也许我的程序逻辑有一些缺陷。
我在这里想念什么?
注意:为简化此测试,程序的关闭按钮 window 不会尝试在程序退出之前结束线程(如果已启动),并且不会检查“断开连接”按钮事件如果真的有东西要断开连接。
稍后编辑:我测试了它,在相同的Windows下,也用Python 3.9.1 32bit / wx '4.1.1 msw (phoenix ) wxWidgets 3.1.5'(这个是用32位的pip安装的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, *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)
和外部文件(实际线程)上的代码:
#!/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")
测试 window(在 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()
完成。