wx.TextCtrl(或底层GTK+)的多线程问题

2 投票
4 回答
2765 浏览
提问于 2025-04-16 07:05

我正在开发一个图形界面(GUI),用来启动一个外部的、长期运行的后台程序。这个后台程序可以通过标准输入(stdin)接收命令,并通过标准输出(stdout)和标准错误(stderr)不断打印输出和错误信息。我在GUI中使用了一个wx.TextCtrl对象来输入命令和显示输出。我的代码主要参考了一个关于“如何实现一个shell GUI窗口”的帖子:wxPython: 如何创建一个bash shell窗口?

不过,我的代码采用了“缓冲之前输出”的方法,也就是说,我使用了一个线程来缓存输出。只有在我输入下一个命令并按下“回车”键时,缓存的输出才会被显示出来。现在,我希望能及时看到输出信息,因此我想要一个功能,让输出能够随时从后台进程直接打印出来,同时我也可以通过标准输入输入命令并看到输出。

class BashProcessThread(threading.Thread):
    def __init__(self, readlineFunc):
        threading.Thread.__init__(self)
        self.readlineFunc = readlineFunc
        self.lines = []
        self.outputQueue = Queue.Queue()
        self.setDaemon(True)

    def run(self):
        while True:
           line = self.readlineFunc()
           self.outputQueue.put(line)
           if (line==""):
            break
        return ''.join(self.lines)

    def getOutput(self):
        """ called from other thread """            
        while True:
            try:
                line = self.outputQueue.get_nowait()
                lines.append(line)
            except Queue.Empty:
                break
        return ''.join(self.lines)

class myFrame(wx.Frame):
    def __init__(self, parent, externapp):
        wx.Window.__init__(self, parent, -1, pos=wx.DefaultPosition)
        self.textctrl = wx.TextCtrl(self, style=wx.TE_PROCESS_ENTER|wx.TE_MULTILINE)
        launchcmd=["EXTERNAL_PROGRAM_EXE"]
        p = subprocess.Popen(launchcmd, stdin=subprocess.PIPE, 
                stdout=subprocess.PIPE, stderr=subprocess.PIPE)
        self.outputThread = BashProcessThread(p.stdout.readline)
        self.outputThread.start()
        self.__bind_events()         
        self.Fit()

    def __bind_events(self):
        self.Bind(wx.EVT_TEXT_ENTER, self.__enter)

    def __enter(self, e):
        nl=self.textctrl.GetNumberOfLines()
        ln =  self.textctrl.GetLineText(nl-1)
        ln = ln[len(self.prompt):]     
        self.externapp.sub_process.stdin.write(ln+"\n")
        time.sleep(.3)
        self.textctrl.AppendText(self.outputThread.getOutput())

我应该如何修改上面的代码来实现这个功能?我还需要使用线程吗?我可以这样写一个线程吗?

class PrintThread(threading.Thread):
    def __init__(self, readlineFunc, tc):
        threading.Thread.__init__(self)
        self.readlineFunc = readlineFunc
        self.textctrl=tc
        self.setDaemon(True)

    def run(self):
        while True:
            line = self.readlineFunc()
            self.textctrl.AppendText(line)

但是,当我尝试使用上面的代码时,它崩溃了。

我遇到了Gtk的错误,如下所示。

(python:13688): Gtk-CRITICAL **: gtk_text_layout_real_invalidate: assertion `layout->wrap_loop_count == 0' failed
Segmentation fault

或者有时出现这个错误

 (python:20766): Gtk-CRITICAL **: gtk_text_buffer_get_iter_at_mark: assertion `GTK_IS_TEXT_MARK (mark)' failed
Segmentation fault

或者有时出现这个错误

(python:21257): Gtk-WARNING **: Invalid text buffer iterator: either the iterator is uninitialized, or the characters/pixbufs/widgets in the buffer have been modified since the iterator was created.
You must use marks, character numbers, or line numbers to preserve a position across buffer modifications.
You can apply tags and insert marks without invalidating your iterators,
but any mutation that affects 'indexable' buffer contents (contents that can be referred to by character offset)
will invalidate all outstanding iterators
Segmentation fault

或者有时出现这个错误

Gtk-ERROR **: file gtktextlayout.c: line 1113 (get_style): assertion failed: (layout->one_style_cache == NULL)
aborting...
Aborted

或者其他错误信息,但每次的错误信息都不同,真的很奇怪!

看起来wx.TextCtrl或者GTK+的底层GUI控件在多线程方面有问题。有时我甚至没有输入任何命令,它也会崩溃。我在网上查找了一些帖子,发现从一个次级线程调用GUI控件是很危险的。

我发现了我的错误。正如在wxpython -- 线程和窗口事件中指出的,或者在Noel和Robin的书《WxPython in action》第18章中提到的:

最重要的一点是,GUI操作必须在主线程中进行,也就是应用程序循环运行的线程。在一个单独的线程中运行GUI操作会导致你的应用程序以不可预测和难以调试的方式崩溃……

我的错误在于我试图将对象传递给另一个线程。这是不对的。我会重新考虑我的设计。

4 个回答

1

pty.spawn() 这个函数可能会很有用。另外,你也可以用 pty.openpty() 手动创建伪终端(PTY),然后把它们作为输入输出传给 popen。

如果你能访问文本模式程序的源代码,你也可以在里面禁用缓冲。

1

你难道不能直接把TextCtrl对象传给你的OutputThread,然后让它直接往输出里添加文本,而不需要和__enter方法绑定在一起吗?

1

最大的问题是,你无法强制子进程不对输出进行缓存。大多数程序的标准输入输出库在输出通过管道时会缓存输出(更准确地说,它们会从行缓存变成块缓存)。像 Expect 这样的工具通过在伪终端中运行子进程来解决这个问题,这基本上是让子进程误以为它的输出是发送到一个终端。

有一个叫做 Pexpect 的 Python 模块,它以与 Expect 相同的方式解决了这个问题。我自己没用过,所以要小心哦。

撰写回答