【问题标题】:Tkinter: Wait for item in queueTkinter:等待队列中的项目
【发布时间】:2011-10-31 18:35:04
【问题描述】:

我正在使用队列在后台线程和 Tk GUI 应用程序之间交换消息。目前,这是通过不时调用查询方法来完成的。

def read_queue(self):
    try:
        self.process(self.queue.get(False)) # non-blocking
    except Queue.Empty:
        pass
    finally:
        self.after(UPDATE_TIME, self.read_queue)

这种方法的问题是,如果UPDATE_TIME 太大,应用程序处理新项目的速度会比可能的慢。如果太小,Tk 会花费大部分时间检查队列,但同时它可以做其他事情。

有没有办法在有新项目到达队列时自动触发read_queue 方法? (当后台线程填满队列时,我当然可以在 Tk 上调用一个方法,但我担心这会给我带来一些并发问题——毕竟这就是我使用队列的原因。)

【问题讨论】:

标签: python multithreading event-handling tkinter


【解决方案1】:

一个选项可能是 mtTkinter http://tkinter.unpythonic.net/wiki/mtTkinter

下面是从后台线程使用 event_generate 的另一个示例:

##The only secure way I found to make Tkinter mix with threads is to never  
##issue commands altering the graphical state of the application in another  
##thread than the one where the mainloop was started. Not doing that often  
##leads to random behaviour such as the one you have here. Fortunately, one  
##of the commands that seems to work in secondary threads is event_generate,  
##giving you a means to communicate between threads. If you have to pass  
##information from one thread to another, you can use a Queue.
##
##This obviously complicates things a bit, but it may work far better.  
##Please note that the 'when' option *must* be specified in the call to  
##event_generate and *must not* be 'now'. If it's not specified or if it's  
##'now', Tkinter may directly execute the binding in the secondary thread's  
##context. (Eric Brunel)

import threading
import time
import Queue
from Tkinter import *

## Create main window
root = Tk()

## Communication queue
commQueue = Queue.Queue()

## Function run in thread
def timeThread():
    curTime = 0
    while 1:
        ## Each time the time increases, put the new value in the queue...
        commQueue.put(curTime)
        ## ... and generate a custom event on the main window
        try:
            root.event_generate('<<TimeChanged>>', when='tail')
        ## If it failed, the window has been destoyed: over
        except TclError:
            break
        ## Next
        time.sleep(1)
        curTime += 1

## In the main thread, do usual stuff
timeVar = IntVar()
Label(root, textvariable=timeVar, width=8).pack()

## Use a binding on the custom event to get the new time value
## and change the variable to update the display
def timeChanged(event):
    timeVar.set(commQueue.get())

root.bind('<<TimeChanged>>', timeChanged)

## Run the thread and the GUI main loop
th=threading.Thread(target=timeThread)
th.start()

root.mainloop()

还提到了以类似的方式使用 after_idle。
即。 root.after_idle(timeChanged)

【讨论】:

  • 使用generate_event 在概念上更具吸引力,@noob oddy 的两个示例正在发挥作用。使用它们作为基础,我嵌入了一个 matplotlib 图形,以便创建一个通过网络检索数据的实时绘图。这在 Linux 中正常工作,但在 Windows 中不能正常工作(XP、7、8.1 都以类似的方式运行)。问题似乎与程序启动时发生的 event_generate 调用有关。可以通过等待所有已经积累的数据到达来避免它然后生成一个事件。但错误消息让我相信event_generate 在 Windows 中不是线程安全的
  • mtTkinter 解决了 windows 问题(设法在 80.000 个事件的爆发中幸存下来)。这以间接的方式证实了这是一个线程问题。 mtTkinter 在幕后使用after 方法,所以我认为它的使用意义不大。
  • 我同时发现 Tkinter 在我的 Windows Python 安装中编译时没有线程支持,而 Linux 安装编译时使用线程支持(至少 mtTkinter 使用 root.globalgetvar('tcl_platform(threaded)') 报告)。这可能是行为差异的原因。
【解决方案2】:

总结:我不会使用“noob oddy's example code”——这是一种根本上存在缺陷的方法。

我不是 python 专家,但“noob oddy”(在后台线程中调用 root.event_generate(...))提供的示例代码似乎是一种“有根本缺陷的方法”。即,互联网上有几篇文章指出“永远不要在'GUI线程'的上下文之外调用Tkinter函数/对象方法”(通常是主线程)。 他的示例“大部分时间”都有效,但如果您提高事件生成率,则示例的“崩溃率”将会增加 - 但是,具体行为取决于事件生成率和平台的性能特征。

例如,在 Python 2.7.3 中使用他的代码,如果你改变:

       time.sleep(1)

到:

       time.sleep(0.01)

那么脚本/应用程序通常会在“x”次迭代后崩溃。

经过大量搜索,如果您“必须使用 Tkinter”,那么从后台线程获取信息到 GUI 线程的最“防弹方法”似乎是使用 'after()' 小部件方法来轮询线程-安全对象(例如“队列”)。例如,

################################################################################
import threading
import time
import Queue
import Tkinter      as Tk
import Tkconstants  as TkConst
from ScrolledText import ScrolledText
from tkFont       import Font

global top
global dataQ
global scrText

def thread_proc():
    x = -1
    dataQ.put(x)
    x = 0
    for i in xrange(5):
        for j in xrange(20):
            dataQ.put(x)
            time.sleep(0.1)
            x += 1
        time.sleep(0.5)
    dataQ.put(x)

def on_after_elapsed():
    while True:
        try:
            v = dataQ.get(timeout=0.1)
        except:
            break
        scrText.insert(TkConst.END, "value=%d\n" % v)
        scrText.see(TkConst.END)
        scrText.update()
    top.after(100, on_after_elapsed)

top     = Tk.Tk()
dataQ   = Queue.Queue(maxsize=0)
f       = Font(family='Courier New', size=12)
scrText = ScrolledText(master=top, height=20, width=120, font=f)
scrText.pack(fill=TkConst.BOTH, side=TkConst.LEFT, padx=15, pady=15, expand=True)
th = threading.Thread(target=thread_proc)
th.start()
top.after(100, on_after_elapsed)
top.mainloop()
th.join()
## end of file #################################################################

【讨论】:

  • 感谢您的澄清。我忘了我问过这个问题。在我的代码中,我最终也使用after 确定了几乎相同的解决方案(并添加了一些 GUI 元素来微调等待时间——性能因操作系统而略有不同),并且没有与 Tk 的内部事件管理器本身搞混.
  • 此代码有错误。在thread_proc 中,您无需先定义v,就可以使用dataQ.put(v)
  • 您应该删除 while True,将 try/except/break 替换为简单的 if,并将 get(timeout=0.1) 替换为 unblocking get:相同的结果,更清晰(因此,更健壮) 代码。 (并且您的应用不会每 100 毫秒释放 100 毫秒等待您的队列)
  • -1,noob 奇怪的方式不是“根本缺陷”,event_generate(when='tail') 可以从另一个线程调用。请参阅 Tkinter-discuss 邮件列表上的 Waking up the Tk event loop from a thread 主题:event generate () with a virtual event works like a charm Guido 说。它避免了轮询(更具反应性的 GUI,更少的功耗),并且已知 after 配方在 MacOsX 上不稳定(仅与其他 GUI 事件反应:窗口焦点、鼠标移动......)。如果您将您的答案改写为“一个常见的食谱是……”而不是“其他人错了,我是对的”,我将删除我的反对票。
  • @Bryan Oakley 可能是一个错字,我认为在thread_proc 中应该使用dataQ.put(x)。遗憾的是代码没有更新以反映您的评论以及@Julien Palard's。
【解决方案3】:

通过使用 os.pipe 在两个线程之间进行同步,可以从 Ken Mumme 解决方案中消除轮询。

tkinter 有一个 createFilehandler 方法,可用于将文件描述符添加到 tk 的选择循环中。然后,您可以通过将字节写入管道来表示队列中的某些内容已准备就绪。

解决方案如下:

import Queue
import os

uiThreadQueue = Queue.Queue() ;

pipe_read, pipe_write = os.pipe() ;

# call one function from the queue.  Triggered by the 
# pipe becoming readable through root.tk.createfilehandler().
def serviceQueue(file, mask):
    os.read(pipe_read, 1) 
    func = uiThreadQueue.get() ;
    func() 

# enqueue a function to be run in the tkinter UI thread.
# best used as inUIThread(lambda: self.callSomeFunction(arg1,arg2,arg3))
def inUIThread(f):
    uiThreadQueue.put(f)
    os.write(pipe_write, "x")

... set up your widgets, start your threads, etc.....


root.tk.createfilehandler(pipe_read, tkinter.READABLE, serviceQueue)
root.mainloop()

我不是 python 专家;如果我搞砸了任何编码约定,我深表歉意。不过,我很擅长管道:)

【讨论】:

  • 仅供参考,Windows 上不支持createfilehandler(),轮询队列是您能做的最好的选择。
  • 多么棒的解决方案!唯一一个在 MAIN 线程循环中监听事件。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-05-04
  • 2015-09-12
  • 2019-11-17
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多