【问题标题】:wxPython threads blockingwxPython 线程阻塞
【发布时间】:2015-12-04 16:26:54
【问题描述】:

这是在 wxPython 的 Phoenix 分支中。

为了不阻塞 GUI,我正在尝试运行几个线程。

我的两个线程工作正常,但另一个似乎从未达到其绑定的结果函数。我可以说它正在运行,只是似乎没有正确发布事件。

这是主计算线程的结果函数:

def on_status_result(self, event):
    if not self.panel.progress_bar.GetRange():
        self.panel.progress_bar.SetRange(event.data.parcel_count)
    self.panel.progress_bar.SetValue(event.data.current_parcel)
    self.panel.status_label.SetLabel(event.data.message)

我是这样绑定它们的:

from wx.lib.pubsub.core import Publisher
PUB = Publisher()

这是我绑定方法的方式:

def post_event(message, data):
    wx.CallAfter(lambda *a: Publisher().sendMessage(message, data=data))

这里是线程。第一个不起作用,但第二个起作用:

class PrepareThread(threading.Thread):
    def __init__(self, notify_window):
        threading.Thread.__init__(self)
        self._notify_window = notify_window
        self._want_abort = False

    def run(self):
        while not self._want_abort:
            for status in prepare_collection(DATABASE, self._previous_id, self._current_id, self._year, self._col_type,
                                             self._lock):
                post_event('prepare.running', status)
        post_event('prepare.complete', None)
        return None

    def abort(self):
        self._want_abort = True


class SetupThread(threading.Thread):
    def __init__(self, notify_window):
        threading.Thread.__init__(self)
        self._notify_window = notify_window
        self._want_abort = False

    def run(self):
        while not self._want_abort:
            do_more_stuff_with_the_database()
            return None

    def abort(self):
        self._want_abort = True


class LatestCollectionsThread(threading.Thread):
    def __init__(self, notify_window):
        threading.Thread.__init__(self)
        self._notify_window = notify_window
        self._want_abort = False

    def run(self):
        while not self._want_abort:
            do_stuff_with_my_database()
            return None

    def abort(self):
        self._want_abort = True

prepare_collection 是一个产生Status 对象的函数,如下所示:

class Status:
    def __init__(self, parcel_count, current_parcel, total, message):
        self.parcel_count = parcel_count
        self.current_parcel = current_parcel
        self.total = total
        self.message = message

以下是我创建/启动/订阅 PrepareThread 的方式:

MainForm(wx.Form):
    prepare_thread = PrepareThread(self)
    prepare_thread.start()

    self.pub = Publisher()
    self.pub.subscribe(self.on_status_result, 'prepare.running')
    self.pub.subscribe(self.on_status_result, 'prepare.complete')

    def on_status_result(self, event):
        if not self.panel.progress_bar.GetRange():
            self.panel.progress_bar.SetRange(event.data.parcel_count)
        self.panel.progress_bar.SetValue(event.data.current_parcel)
        self.panel.status_label.SetLabel(event.data.message)

我尝试使用range(10)prepare_collection 存根,但我仍然没有点击事件处理程序。

【问题讨论】:

  • 嘿,摩根,对不起……我今晚可能没有机会回顾这个……只是超级忙:/
  • @joran 一切都好。
  • 哦,天哪......对不起......我会尽力帮助你这个周末这真是一个疯狂的一周
  • @JoranBeasley 没问题。
  • 顺便说一句 - 我猜你是here?某些代码看起来就是您正在尝试调整的内容 - 对吗?

标签: python multithreading wxpython


【解决方案1】:

问题是事件系统最终会从线程本身调用更新函数(事件处理程序),你几乎不应该这样做(基本上你最终会遇到奇怪的竞争条件和工件)......总是让主线程回调。

wxPython 已经考虑到了这一点,任何使用 wx.CallAfter 调用的方法都将从始终在主线程中运行的主程序循环中调用。这与 wx.pubsub 模块相结合使您可以轻松创建自己的事件框架工作......像这样

def MyPostEvent(event_name,event_data):
  #just a helper that triggers the event with wx.CallAfter
  wx.CallAfter(lambda *a:Publisher().sendMessage(event_name,data=event_data))

#then to post an event

MyPostEvent("some_event.i_made_up",{"payload":True})

#then in your main thread subscribe 

def OnEventHandler(evt):
  print "EVT.data",evt.data

pub = Publisher()
pub.subscribe("some_event.i_made_up",OnEventHandler)

【讨论】:

  • “some_event.i_made_up”是否与我传递给 MyPostEvent 的内容相同?另外,这对 Phoenix 有效吗?
  • 它应该在 phoenix 上工作(可能需要一些小的修改......但这个概念一般适用)
  • 是的,这就是您要传递的内容:P(请参阅接收和发出事件的示例)
  • 哎呀,呃。这就是我下班后尝试阅读代码的结果。我明天在工作中试试这个,然后给你接受。
  • 它可能会或可能不会真正解决问题......但这就是我要开始的地方
【解决方案2】:

这可能是一个相当复杂的答案,而且很难准确计算出代码的每个部分中都有哪些 sn-ps(即它们都存在于哪些文件中)。我假设您想保留 pubsub 这样做的方法(我认为这是一个好主意)。如果您的实际项目的结构非常复杂,您可能需要比我在这里使用的更复杂的Publisher 管理 - 请告诉我...


这里 - 我先放个剧透 - 这是一个one file solution ,你似乎想要的 - 一个带有按钮的面板,用于启动准备线程、状态栏和准备完成时的处理程序。在 Python 2.7 上使用 wxPython Phoenix 64 位 Python 3 和老式 wxPython 进行了测试。两者都在 Windows 上 - 但如果需要,我可以在 Linux 机器中启动它。

总结该文件的重要(非样板)位

您需要一个 Publisher 对象,您的线程将消息发送到您的主线程(我猜是您的示例中的MainForm)订阅。您可以为每个线程管理一个Publisher,但我认为您只需要一个用于PrepareThread,所以我现在将使用该模型。

在文件的顶部,使用

from wx.lib.pubsub import pub

这让pubsub 管理实例化单个Publisher 对象。

在您的线程中,正如您所做的那样,在那里发布消息 - 对您的 post_event 助手稍作修改:

def post_event(message, data):
    wx.CallAfter(lambda *a: pub.sendMessage(message, data=data))

在您的主线程中 - 订阅这些消息。我会说每条消息有一个处理程序通常最容易,而不是像你一样将两个不同的消息发送到同一个处理程序,所以我选择了

pub.subscribe(self.on_status_result, 'prepare.running')
pub.subscribe(self.on_status_finished, 'prepare.complete')

您可以保留on_status_result 原样并定义一个类似的on_status_finished。在我的示例中,我稍后启用了一个新按钮来让您做一些实际的工作。

注意在命名消息的有效负载时需要小心 - pubsub 推断出相当多的关于它所期望的信息的信息,一开始我就被它吸引了。


附:在准备这个答案的最后——我找到了this blog post。它说的类似于我上面的内容,所以我不会复制它,但他们使用另一种实例化 Publisher() 的方法,就像你原来的例子一样——暗示这也应该有效。您可能更喜欢那里的措辞。同样 - 您可能会发现 this wxPython wiki 页面很有用。

【讨论】:

  • 这看起来正是我所需要的。给我一个小时左右,然后我会试试看!
  • 干得好..抱歉我很忙...在 wx2.8 中调用发布者 sendMessage 订阅在所有实例之间共享(实际上 Publisher() 可能返回相同的实例...)
  • 谢谢@JoranBeasley - 这是我不知道的好信息 - 我对 pubsub 框架并不熟悉 - 只是使用 wx 给一些代码提供了一个漂亮的前端.我会给你一个upvote,但当问题第一次出现时我已经这样做了。
  • 不,不,你的回答很好+1 ...我的回答对于wx3.0实际上是错误的,所以:/(但它仍然有助于达成我认为的可行解决方案)
  • @joran Cool - 是的 - 我认为确实如此。