无法理解为什么在Windows下的(wx)Python中正常停止一个线程会挂起该线程的剩余执行代码

问题描述

我有一个 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 下)看起来像这样(点击工具栏上的“连接”按钮后显示在这里):

wx interface in 'connected' status

解决方法

不得不承认这起初是不直观的。试图省略 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() 完成。