2011-08-21 45 views
10

我正在使用隊列在後臺線程和Tk GUI應用程序之間交換消息。目前,這是通過每隔一段時間調用一次查詢方法完成的。Tkinter:等待隊列中的項目

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的方法,當後臺線程填充隊列,但我擔心,這給了我一些併發問題 - 這就是爲什麼我使用隊列畢竟。)

+2

顯然,您可以使用後臺線程中的event_generate在GUI中觸發虛擬事件。也許這可以用作關於隊列狀態的一種通知。 http://groups.google.com/group/comp.lang.python/browse_thread/thread/3476fd30bec12367/853bb6f6dd216960?lnk=gst&q=brunel+%2Bevent_generate#853bb6f6dd216960 –

+0

似乎有效。隨意添加它作爲一個真正的答案。 – Debilski

回答

5

摘要:我不會使用「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 ################################################################# 
+0

謝謝澄清。我忘了我曾問過這個問題。在我的代碼中,我最終使用'after'來解決大部分相同的解決方案(並添加了一些GUI元素來微調等待時間 - 根據操作系統的不同,性能會有所不同),而不是與Tk的內部事件管理器本身混雜在一起。 – Debilski

+1

此代碼有一個錯誤。在'thread_proc'中,你不需要首先定義'v'就可以執行'dataQ.put(v)'。 –

+1

您應該刪除while True,用簡單的if替換try/except/break,並用get unblocking替換get(timeout = 0.1)get:結果相同,代碼更清晰(如此更魯棒)。 (並且你的應用不會每隔100ms釋放100ms等待你的隊列) –

13

一種選擇可能是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

使用'generate_event'在概念上更吸引人,@noob oddy的兩個例子正在運行。使用它們作爲基礎,我嵌入了一個matplotlib圖形,以創建一個通過網絡檢索數據的實時繪圖。這在Linux中正常工作,但不是在Windows中(XP,7,8.1都以類似的方式運行)。這個問題似乎與程序啓動時突發的event_generate調用有關。可以通過等待所有已經累積的數據到達然後生成一個事件。但是這些錯誤消息讓我相信'event_generate' **在Windows中不是線程安全的。** – NameOfTheRose

+0

** mtTkinter **解決了窗口問題(設法在80.000個事件爆發後倖存下來)。這以間接的方式證實了這是一個線程問題。 ** mtTkinter **,在場景後面,使用'after'方法,所以我沒有看到它的用處。 – NameOfTheRose

+0

我同時發現Tkinter在我的Windows Python安裝中沒有線程支持的情況下編譯,而Linux安裝是用線程支持編譯的(至少這是mtTkinter使用'root.globalgetvar('tcl_platform(threaded) 「)')。這可能是行爲差異的原因。 – NameOfTheRose

2

通過使用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() 

我不是一個Python專家;如果我弄亂了任何編碼習慣,道歉。我很擅長管道,但:) :)

+2

僅供參考在Windows上沒有'createfilehandler()'支持,輪詢隊列是最好的。 – shuckc