【问题标题】:Beginner-level Python threading problems初学者级别的 Python 线程问题
【发布时间】:2009-01-27 03:38:45
【问题描述】:

作为 Python(使用 pyGTK)GUI 开发的新手,我刚刚开始学习线程。为了测试我的技能,我编写了一个简单的带有开始/停止按钮的小 GTK 界面。目标是当它被点击时,一个线程启动,在文本框中快速增加一个数字,同时保持 GUI 响应。

我的 GUI 工作得很好,但是线程有问题。这可能是一个简单的问题,但我的脑子里全是煎熬。下面我首先粘贴了 Python 解释器的引用,然后是代码。你可以去http://drop.io/pxgr5id下载。我正在使用 bzr 进行修订控制,因此如果您想进行修改并重新删除它,请提交更改。我也将代码粘贴到http://dpaste.com/113388/,因为它可以有行号,而这个降价的东西让我很头疼。

美国东部时间 1 月 27 日 15:52 更新: 稍微更新的代码可以在这里找到:http://drop.io/threagui/asset/thread-gui-rev3-tar-gz

追溯

crashsystems@crashsystems-laptop:~/Desktop/thread-gui$ python threadgui.pybtnStartStop clicked
Traceback (most recent call last):
  File "threadgui.py", line 39, in on_btnStartStop_clicked
    self.thread.stop()
  File "threadgui.py", line 20, in stop
    self.join()
  File "/usr/lib/python2.5/threading.py", line 583, in join
    raise RuntimeError("cannot join thread before it is started")
RuntimeError: cannot join thread before it is started
btnStartStop clicked
threadStop = 1
btnStartStop clicked
threadStop = 0
btnStartStop clicked
Traceback (most recent call last):
  File "threadgui.py", line 36, in on_btnStartStop_clicked
    self.thread.start()
  File "/usr/lib/python2.5/threading.py", line 434, in start
    raise RuntimeError("thread already started")
RuntimeError: thread already started
btnExit clicked
exit() called

代码

#!/usr/bin/bash
import gtk, threading

class ThreadLooper (threading.Thread):
    def __init__ (self, sleep_interval, function, args=[], kwargs={}):
        threading.Thread.__init__(self)
        self.sleep_interval = sleep_interval
        self.function = function
        self.args = args
        self.kwargs = kwargs
        self.finished = threading.Event()

    def stop (self):
        self.finished.set()
        self.join()

    def run (self):
        while not self.finished.isSet():
            self.finished.wait(self.sleep_interval)
            self.function(*self.args, **self.kwargs)

class ThreadGUI:
    # Define signals
    def on_btnStartStop_clicked(self, widget, data=None):
        print "btnStartStop clicked"
        if(self.threadStop == 0):
            self.threadStop = 1
            self.thread.start()
        else:
            self.threadStop = 0
            self.thread.stop()
        print "threadStop = " + str(self.threadStop)

    def on_btnMessageBox_clicked(self, widget, data=None):
        print "btnMessageBox clicked"
        self.lblMessage.set_text("This is a message!")
        self.msgBox.show()

    def on_btnExit_clicked(self, widget, data=None):
        print "btnExit clicked"
        self.exit()

    def on_btnOk_clicked(self, widget, data=None):
        print "btnOk clicked"
        self.msgBox.hide()

    def on_mainWindow_destroy(self, widget, data=None):
        print "mainWindow destroyed!"
        self.exit()

    def exit(self):
        print "exit() called"
        self.threadStop = 1
        gtk.main_quit()

    def threadLoop(self):
        # This will run in a thread
        self.txtThreadView.set_text(str(self.threadCount))
        print "hello world"
        self.threadCount += 1

    def __init__(self):
        # Connect to the xml GUI file
        builder = gtk.Builder()
        builder.add_from_file("threadgui.xml")

        # Connect to GUI widgets
        self.mainWindow = builder.get_object("mainWindow")

        self.txtThreadView = builder.get_object("txtThreadView")
        self.btnStartStop = builder.get_object("btnStartStop")
        self.msgBox = builder.get_object("msgBox")
        self.btnMessageBox = builder.get_object("btnMessageBox")
        self.btnExit = builder.get_object("btnExit")
        self.lblMessage  = builder.get_object("lblMessage")
        self.btnOk = builder.get_object("btnOk")

        # Connect the signals
        builder.connect_signals(self)

        # This global will be used for signaling the thread to stop.
        self.threadStop = 1

        # The thread
        self.thread = ThreadLooper(0.1, self.threadLoop, (1,0,-1))
        self.threadCounter = 0

if __name__ == "__main__":
    # Start GUI instance
    GUI = ThreadGUI()
    GUI.mainWindow.show()
    gtk.main()

【问题讨论】:

    标签: python multithreading pygtk


    【解决方案1】:

    如果你想正确地使用 PyGTK 进行线程化,那就有点棘手了。基本上,您不应从主线程以外的任何其他线程中更新 GUI(GUI 库中的常见限制)。通常这是在 PyGTK 中使用队列消息机制(用于工作人员和 GUI 之间的通信)完成的,这些消息使用超时函数定期读取。一旦我在本地 LUG 上就该主题进行了演示,您可以从 Google Code repository 获取此演示的示例代码。看看forms/frmmain.py 中的MainWindow 类,特别是_pulse() 方法和on_entry_activate() 中所做的事情(线程在那里启动并创建了空闲计时器)。

    def on_entry_activate(self, entry):
        text = entry.get_text().strip()
        if text:
            store = entry.get_completion().get_model()
            if text not in [row[0] for row in store]:
                store.append((text, ))
            thread = threads.RecommendationsFetcher(text, self.queue)# <- 1
            self.idle_timer = gobject.idle_add(self._pulse)# <- 2
            tv_results = self.widgets.get_widget('tv_results')
            model = tv_results.get_model()
            model.clear()
            thread.setDaemon(True)# <- 3
            progress_update = self.widgets.get_widget('progress_update')
            progress_update.show()
            thread.start()# <- 4
    

    这样,应用程序在“空闲”时更新 GUI(通过 GTK 表示)不会导致冻结。

    • 1:创建线程
    • 2:创建空闲计时器
    • 3:守护线程,这样应用就可以在不等待线程完成的情况下关闭
    • 4:启动线程

    【讨论】:

      【解决方案2】:

      一般情况下,最好尽量避免使用线程。正确编写线程应用程序非常困难,更难知道自己是否正确。由于您正在编写一个 GUI 应用程序,因此您可以更轻松地可视化如何执行此操作,因为您已经必须在异步框架中编写应用程序。

      需要认识到的重要一点是,GUI 应用程序实际上什么也没做。它大部分时间都在等待操作系统告诉它发生了什么事。只要你知道如何编写长时间运行的代码,你就可以在这段空闲时间做很多事情,这样它就不会阻塞。

      您可以通过使用超时来解决您最初的问题;告诉您的 GUI 框架在延迟后回调某个函数,然后重置该延迟或开始另一个延迟调用。

      另一个常见问题是如何在 GUI 应用程序中通过网络进行通信。网络应用程序就像 GUI 应用程序一样,它们会进行大量等待。使用网络 IO 框架(如 Twisted)可以轻松地让应用程序的两个部分合作等待而不是竞争,并再次减少对额外线程的需求。

      长时间运行的计算可以迭代而不是同步编写,您可以在 GUI 空闲时进行处理。您可以在 python 中使用生成器轻松完成此操作。

      def long_calculation(param, callback):
          result = None
          while True:
              result = calculate_next_part(param, result)
              if calculation_is_done(result):
                  break
              else:
                  yield
          callback(result)
      

      调用long_calculation 会给你一个生成器对象,在生成器对象上调用.next() 将运行生成器,直到它到达yieldreturn。您只需告诉 GUI 框架在有时间时调用 long_calculation(some_param, some_callback).next,最终您的回调将被调用并返回结果。

      我不太了解 GTK,所以我不能告诉你应该调用哪些 gobject 函数。不过,有了这个解释,您应该能够在文档中找到必要的功能,或者最糟糕的是,在相关的 IRC 频道上询问。

      不幸的是,没有好的一般情况答案。如果您明确说明您要做什么,那么解释为什么在这种情况下不需要线程会更容易。

      【讨论】:

      • launchpad.net/rotor-control-software 为什么我不需要 RCS 类中的线程,特别是从 GUI 调用 Start() 时?我在这里发布的这个小虚拟程序实际上只是一个让线程工作的练习。
      • 如果您可以获得串行接口的文件描述符,您可能可以将其设置为非阻塞模式并在接口上进行非阻塞读取和写入。不过,我不建议使用该库。我很快看了一遍,写得不是很好。
      • 很好听!不幸的是,转子只有一个串行接口。知道任何体面的 Python 串行模块吗?
      • pyserial.wiki.sourceforge.net/pySerial 在我看来,如果你想做跨平台串行端口应用程序,python 与 pySerial 是所有语言/技术中最好的选择。
      【解决方案3】:

      您不能重新启动已停止的线程对象;不要尝试。相反,如果您想在对象真正停止并加入后重新启动它,请创建对象的新实例。

      【讨论】:

        【解决方案4】:

        我使用过不同的工具来帮助清理线程、空闲处理等工作。

        make_idle 是一个函数装饰器,可以让你在后台协同运行一个任务。这是一个很好的中间地带,介于足够短以在 UI 线程中运行一次且不影响应用程序的响应能力和在特殊同步中执行完整线程之间。在装饰函数内部,您使用“yield”将处理交还给 GUI,以便它可以保持响应,并且下次 UI 空闲时,它将在您停止的函数中继续。所以要开始这个,你只需调用 idle_add 到装饰函数。

        def make_idler(func):
            """
            Decorator that makes a generator-function into a function that will
        continue execution on next call
            """
            a = []
        
            @functools.wraps(func)
            def decorated_func(*args, **kwds):
                if not a:
                    a.append(func(*args, **kwds))
                try:
                    a[0].next()
                    return True
                except StopIteration:
                    del a[:]
                    return False
        
            return decorated_func
        

        如果您需要进行更多处理,可以在需要时使用上下文管理器锁定 UI 线程,以帮助使代码更安全

        @contextlib.contextmanager
        def gtk_critical_section():
            gtk.gdk.threads_enter()
            try:
                yield
            finally:
                gtk.gdk.threads_leave()
        

        这样你就可以了

        with gtk_critical_section():
            ... processing ...
        

        我还没有完成它,但是在结合纯粹在空闲和纯粹在线程中做事时,我有一个装饰器(尚未测试因此未发布),您可以告诉它是否在 yield 之后的下一部分是在 UI 的空闲时间或线程中运行。这将允许在 UI 线程中进行一些设置,切换到新线程来处理后台工作,然后切换到 UI 的空闲时间进行清理,从而最大限度地减少对锁的需求。

        【讨论】:

          【解决方案5】:

          我没有详细查看您的代码。但我看到您的问题有两种解决方案:

          根本不要使用线程。而是使用超时,如下所示:

          import gobject
          
          i = 0
          def do_print():
              global i
              print i
              i += 1
              if i == 10:
                  main_loop.quit()
                  return False
              return True
          
          main_loop = gobject.MainLoop()
          gobject.timeout_add(250, do_print)
          main_loop.run()
          

          当使用线程时,您必须确保您的 GUI 代码只能同时从一个线程调用,方法是这样保护它:

          import threading
          import time
          
          import gobject
          import gtk
          
          gtk.gdk.threads_init()
          
          def run_thread():
              for i in xrange(10):
                  time.sleep(0.25)
                  gtk.gdk.threads_enter()
                  # update the view here
                  gtk.gdk.threads_leave()
              gtk.gdk.threads_enter()
              main_loop.quit()
              gtk.gdk.threads_leave()
          
          t = threading.Thread(target=run_thread)
          t.start()
          main_loop = gobject.MainLoop()
          main_loop.run()
          

          【讨论】:

            猜你喜欢
            • 1970-01-01
            • 1970-01-01
            • 2011-08-17
            • 1970-01-01
            • 2013-12-20
            • 1970-01-01
            • 2021-02-06
            • 1970-01-01
            • 2020-06-06
            相关资源
            最近更新 更多