【问题标题】:multithreading issue with wx.TextCtrl (or underlying GTK+)wx.TextCtrl(或底层 GTK+)的多线程问题
【发布时间】:2011-05-10 03:56:08
【问题描述】:

我正在开发一个 GUI 来启动一个外部长期运行的后台程序。这个后台程序可以通过 stdin 获得输入命令,并使用 stdout 和 stderr 继续打印输出和错误消息。我在 GUI 中使用 wx.TextCtrl 对象来提供输入和打印输出。我目前的代码如下,主要是受到“如何实现一个shell GUI窗口”帖子的启发:wxPython: how to create a bash shell window?

但是,我的以下代码使用“缓冲前一个输出”方法,即我使用线程来缓冲输出。只有当我给出下一个输入命令并按下“返回”按钮时,才能呈现缓冲的事务输出。现在,我想及时看到输出消息,因此我想具有“输出总是可以从后台子进程自发打印(直接刷新)并且我也可以通过标准输入干扰输入一些输入命令和打印输出。

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 -- threads and window events 或 Noel 和 Robin 在“WxPython in action”一书的第 18 章中指出的那样:

最重要的一点是,GUI 操作必须发生在主线程中,或者应用程序循环运行所在的线程中。在单独的线程中运行 GUI 操作是让您的应用程序在不可预知且难以预料的情况下崩溃的好方法- 调试方式...

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

【问题讨论】:

    标签: python multithreading shell wxpython stdout


    【解决方案1】:

    最大的问题是你不能强制子进程不缓冲它的输出,而且大多数程序的标准 I/O 库会在 stdout 是管道时缓冲输出(更准确地说,它们会从 line-缓冲到块缓冲)。 Expect 之类的工具通过在伪 tty 中运行子进程来解决此问题,这基本上会欺骗子进程,使其认为其输出将发送到终端。

    有一个名为 Pexpect 的 Python 模块,它以与 Expect 相同的方式解决了这个问题。我从未使用过它,所以请注意

    【讨论】:

    • 嗨,马塞洛,感谢您的意见。缓冲来自我的线程,而不是来自后台子进程程序。后台程序没有缓冲其输出消息。即使标准输出管道生成块缓冲区,对于我的应用程序来说,也没关​​系。我只需要一种准时打印交易消息的方法,或者至少不需要等待下一个输入/“返回”按钮。此外,正如我为 GUI 所做的那样,我使用 wx.textctrl 与用户进行交互式输入/输出。我可以为此使用 pty/pexpect 吗?你觉得可以用一个线程(一直打印到wx.TextCtrl)来实现吗?
    • 嗨,马塞洛,感谢您的意见。在纠正我的错误(从辅助线程调用 GUI)之后,我发现问题出在你所说的,为了有更多的想法,我创建了另一个帖子。最后,为了感谢您详细而有见地的回答(我也非常感谢 blaze),我将您的回答视为已接受。
    【解决方案2】:

    pty.spawn() 可能很有用。您也可以使用pty.openpty() 手动创建 PTY 并将它们作为 stdin/stdout 传递给 popen。

    如果您可以访问文本模式程序源,您也可以在那里禁用缓冲。

    【讨论】:

    • 嗨,布雷泽,谢谢您的意见。但我的问题是在 gui 中有一个嵌入式 cli(这就是 wx.textctrl 的用途),以便用户与后台进程进行交互。我不知道你的回答是否解决了这一点。缓冲也来自我的线程,而不是来自后台程序。
    • 您确定要在您的应用中进行缓冲吗?通常这种症状是子进程中缓冲的标志,而不是读取缓冲。无论如何,如果读者缓冲忘记了 readline(),请使用 read1()(而不是 read()!)并自己组装行。
    • 嗨,blaze,我的意思是 readlinefunc 或 BashProcessThread 是我创建的,用于缓冲外部应用程序 (EXTERNAL_PROGRAM_EXE) 的输出。我使用这种方式进行用户交互。但我的方式不可取。我需要一些可以自动刷新事务输出的方法,而无需等待下一个输入/“返回”按钮。 EXTERNAL_PROGRAM_EXE 不做任何缓冲。抱歉误导,我放了更多代码来说明我想问的问题
    • 我不太明白你的问题。是的,您可以读取孩子的标准输出并将其附加到 TextView。如果您使用 readline() 阅读,则在换行符到达标准输出之前,您将不会获得任何数据。如果您需要在写入数据后立即读取数据,请将 BashProcessThread(p.stdout.readline) 替换为 BashProcessThread(p.stdout.read1)
    • 我只是想知道是否可以使用单独的线程在 wx.TextCtrl 上继续注销子进程的标准输出。我尝试使用上面的代码(PrintThread)。这是行不通的。根本没有输出。
    【解决方案3】:

    您不能只将 TextCtrl 对象传递给您的 OutputThread 并让它直接将文本附加到输出而不用 __enter 方法绑定它吗?

    【讨论】:

    • 嗨,Satwik,此解决方案不正确。这是一个错误,请在原帖中查看我的更多 cmets。
    【解决方案4】:

    gtk 中任何东西(wxpython 构建于其上,断言失败的东西)应用程序的调用合同是您必须只修改 gui 控件主 Gui 线程。所以对我来说答案很简单:

    在 C# 中,我需要执行以下操作(这是一个匿名 lambda 函数,几乎可以肯定在 wxpython/gtk 中有类似的库调用)

    Gtk.Application.Invoke((_,__) =>
    {
        //code which can safely modify the gui goes here
    });
    

    这应该可以解决你的断言问题……至少对我有用。

    【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2017-12-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-01-12
    • 2013-06-02
    相关资源
    最近更新 更多