无法理解为什么正常停止线程会在 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_disconnectjoin() 表示它正在等待线程停止。但是,您在线程中的代码试图让 wxPython 执行代码 (self.wx_ui.frame_statusbar.SetStatusText),该代码无法继续,因为 wxPython 循环仍在等待 join() 完成。