Tkinter:等待队列中的项目

时间:2011-08-21 22:12:27

标签: python multithreading event-handling tkinter

我正在使用队列在后台线程和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上调用一个方法,但我担心这会给我带来一些并发问题 - 这就是我毕竟使用队列的原因。)

3 个答案:

答案 0 :(得分:15)

一个选项可能是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)

答案 1 :(得分:5)

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

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

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

       time.sleep(1)

为:

       time.sleep(0.01)

然后脚本/应用程序通常会在'x'次迭代后崩溃。

经过多次搜索,如果你“必须使​​用Tkinter”,那么从后台线程获取信息到GUI线程的最“防弹方法”就是使用'after()'widget方法来轮询线程-safe对象(例如'Queue')。如,

################################################################################
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 #################################################################

答案 2 :(得分:3)

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

tkinter有一个createFilehandler方法,可用于将文件描述符添加到tk的select循环中。然后,您可以通过将一个字节写入管道来发出队列中已准备好的信号。

解决方案如下所示:

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()

我不是蟒蛇专家;如果我搞砸了任何编码约定,请道歉。我很喜欢管道,但是:)