Tkinter GUI,I/O 和线程:何时使用队列,何时使用事件?
Tkinter GUI, I/O & Threading: When to use queues, when events?
我正在使用 TKinter 构建一个 GUI(用于与多通道分析器的套接字连接)以定期(~15 秒)接收和绘制数据(~15.000.000 个值)。
在接收数据时我不希望 GUI 冻结,所以我使用多线程进行连接处理、数据接收和绘图操作。正如在可重现的代码中看到的那样,我通过使用 threading.Event()
设置一个事件并一个接一个地处理线程(initSettings()
和 acquireAndPlotData
中的几行代码)来实现这一点。我唯一一次干扰 GUI 是在绘制到 canvas 时 & 我用 tkinters after()
方法来做到这一点。
启动时,只要 window 打开并按预期工作,代码就不会冻结并接收和绘图。
当我读到在 tkinter GUI 中处理阻塞 I/O 操作时,我只找到了排队和递归检查队列的示例(使用 Queue
& after()
,
1
2
3
4
5
), 但我发现使用 threading.Event()
.
处理这些操作更方便、更容易
现在我的问题是:
我使用的是正确的方法还是我在这里遗漏了一些重要的东西?(关于线程安全,竞争条件,如果绘图失败并且花费的时间超过数据采集怎么办?我的东西没想到?不好的做法?等...)
非常感谢您对此事的反馈!
可重现代码
#####################*** IMPORTS ***#######################################################
import tkinter
from tkinter import ttk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import time
import threading
import numpy as np
################### *** FUNCTIONS *** #########################################################
# *** initializes two threads for initializing connection & receiving/plotting data ***
def onStartButtonClick(event):
#
init_settings_thread.start()
acquire_and_plot_data_thread.start()
#
# *** inizialize connection & set event when finished & ready for sending data ***
def initSettings():
#time.sleep() simulates the time it takes to inizialize the connection
time.sleep(2)
start_data_acquisition_event.set()
# *** waiting for event/flag from initSettings() & start data receiving/plotting loop afer event set ***
def acquireAndPlotData():
start_data_acquisition_event.wait()
while start_data_acquisition_event.is_set():
# time.sleep() simulates the time it takes the connection to fill up the buffer
time.sleep(4)
# send updateGuiFigure to tkinters event queue, so that it won't freeze
root.after(0, updateGuiFigure)
# *** set new data points on existing plot & blit GUI canvas ***
def updateGuiFigure():
# simulate data -> 15.000.000 points in real application
line.set_xdata(np.random.rand(10))
#
line.set_ydata(np.random.rand(10))
#
plotting_canvas.restore_region(background) # restore background
ax.draw_artist(line) # redraw just the line -> draw_artist updates axis
plotting_canvas.blit(ax.bbox) # fill in the axes rectangle
#
# *** update background for resize events ***
def update_background(event):
global background
background = plotting_canvas.copy_from_bbox(ax.bbox)
##########################*** MAIN ***#########################################################
# Init GUI
root = tkinter.Tk()
# Init frame & canvas
frame = ttk.Frame(root)
plotting_area = tkinter.Canvas(root, width=700, height=400)
#
frame.grid(row=0, column=1, sticky="n")
plotting_area.grid(row=0, column=0)
# Init button & bind to function onStartButtonClick
start_button = tkinter.Button(frame, text="Start")
start_button.bind("<Button-1>", onStartButtonClick)
start_button.grid(row=0, column=0)
# Init figure & axis
fig = Figure(figsize=(7, 4), dpi=100)
ax = fig.add_subplot(111)
# Connect figure to plotting_area from GUI
plotting_canvas = FigureCanvasTkAgg(fig, master=plotting_area)
# Set axis
ax.set_title('Test')
ax.grid(True)
ax.set_xlabel('x-axis')
ax.set_ylabel('y-axis')
ax.set(xlim=[0,1], ylim=[0, 1])
# Init plot
line, = ax.plot([], [])
# if animated == True: artist (= line) will only be drawn when manually called draw_artist(line)
line.set_animated(True)
# Draw plot to GUI canvas
plotting_canvas.draw()
plotting_canvas.get_tk_widget().pack(fill=tkinter.BOTH)
background = plotting_canvas.copy_from_bbox(ax.bbox) # cache background
plotting_canvas.mpl_connect('draw_event', update_background) # update background with 'draw_event'
# Init threads
start_data_acquisition_event = threading.Event()
#
init_settings_thread = threading.Thread(name='init_settings_thread', target=initSettings, daemon=True)
acquire_and_plot_data_thread = threading.Thread(name='acquire_and_plot_data_thread', target=acquireAndPlotData, daemon=True)
# Start tkinter mainloop
root.mainloop()
使用多个 类 处理的代码片段示例如下所示(与上面的代码相同,但不可重现,可以忽略):
def onStartButtonClick(self):
#
.
# Disable buttons and get widget values here etc.
.
#
self.start_data_acquisition_event = threading.Event()
self.init_settings_thread = threading.Thread(target=self.initSettings)
self.acquire_and_plot_data_thread = threading.Thread(target=self.acquireAndPlotData)
#
self.init_settings_thread.start()
self.acquire_and_plot_data_thread.start()
# FUNCTION END
def initSettings(self):
self.data_handler.setInitSettings(self.user_settings_dict)
self.data_handler.initDataAcquisitionObject()
self.start_data_acquisition_event.set()
def acquireAndPlotData(self):
self.start_data_acquisition_event.wait()
while self.start_data_acquisition_event.is_set():
self.data_handler.getDataFromDataAcquisitionObject()
self.master.after(0, self.data_plotter.updateGuiFigure)
所以我这样做了,但我不知道它是否适合你,或者这是否是一个好方法,但它可以保护你 .after
正如评论中所述,它有好处是你的函数 do_stuff
只在需要时被调用。
import tkinter as tk
import time
import threading
def get_data():
time.sleep(3)
print('sleeped 3')
_check.set(1)
def do_stuff():
try:
root.configure(bg='#'+str(_var.get()))
_var.set(_var.get()+101010)
except:
_var.set(101010)
root = tk.Tk()
_check = tk.IntVar(value=0)
_var = tk.IntVar(value=101010)
def callback(event=None, *args):
t1 = threading.Thread(target=get_data)
t1.start()
do_stuff()
_check.trace_add('write', callback) #kepp track of that variable and trigger callback if changed
callback() # start the loop
root.mainloop()
一些研究:
interpreter is only valid in the thread that created it, and all Tk
activity must happen in this thread, also. That means that the
mainloop must be invoked in the thread that created the
interpreter. Invoking commands from other threads is possible;
_tkinter will queue an event for the interpreter thread, which will
then execute the command and pass back the result.
#l1493 var_invoke
The current thread is not the interpreter thread. Marshal
the call to the interpreter thread, then wait for
completion. */
if (!WaitForMainloop(self))
return NULL;
is-it-safe-to-use-a-intvar-doublevar-in-a-python-thread
When you set a variable, it calls the globalsetvar method on the
master widget associated with the Variable. The _tk.globalsetvar
method is implemented in C, and internally calls var_invoke, which
internally calls WaitForMainLoop, which will attempt schedule the
command for execution in the main thread, as described in the quote
from the _tkinter source I included above.
Start
|
|<----------------------------------------------------------+
v ^
Do I have No[*] Calculate how Sleep for at |
work to do? -----> long I may sleep -----> most that much --->|
| time |
| Yes |
| |
v |
Do one callback |
| |
+-----------------------------------------------------------+
常识
来自 bugtracker:
Tkinter and threads.
If you want to use both tkinter and threads, the safest method is to
make all tkinter calls in the main thread. If worker threads generate
data needed for tkinter calls, use a queue.Queue to send the data to
the main thread. For a clean shutdown, add a method to wait for
threads to stop and have it called when the window close button [X] is
pressed.
Just run all UI code in the main thread, and let the writers write to
a Queue object; e.g.
结论
你做事的方式和我做事的方式看起来很像 ideal,但它们似乎并没有错。这取决于需要什么。
我正在使用 TKinter 构建一个 GUI(用于与多通道分析器的套接字连接)以定期(~15 秒)接收和绘制数据(~15.000.000 个值)。
在接收数据时我不希望 GUI 冻结,所以我使用多线程进行连接处理、数据接收和绘图操作。正如在可重现的代码中看到的那样,我通过使用 threading.Event()
设置一个事件并一个接一个地处理线程(initSettings()
和 acquireAndPlotData
中的几行代码)来实现这一点。我唯一一次干扰 GUI 是在绘制到 canvas 时 & 我用 tkinters after()
方法来做到这一点。
启动时,只要 window 打开并按预期工作,代码就不会冻结并接收和绘图。
当我读到在 tkinter GUI 中处理阻塞 I/O 操作时,我只找到了排队和递归检查队列的示例(使用 Queue
& after()
,
1
2
3
4
5
), 但我发现使用 threading.Event()
.
现在我的问题是:
我使用的是正确的方法还是我在这里遗漏了一些重要的东西?(关于线程安全,竞争条件,如果绘图失败并且花费的时间超过数据采集怎么办?我的东西没想到?不好的做法?等...)
非常感谢您对此事的反馈!
可重现代码
#####################*** IMPORTS ***#######################################################
import tkinter
from tkinter import ttk
from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg
from matplotlib.figure import Figure
import time
import threading
import numpy as np
################### *** FUNCTIONS *** #########################################################
# *** initializes two threads for initializing connection & receiving/plotting data ***
def onStartButtonClick(event):
#
init_settings_thread.start()
acquire_and_plot_data_thread.start()
#
# *** inizialize connection & set event when finished & ready for sending data ***
def initSettings():
#time.sleep() simulates the time it takes to inizialize the connection
time.sleep(2)
start_data_acquisition_event.set()
# *** waiting for event/flag from initSettings() & start data receiving/plotting loop afer event set ***
def acquireAndPlotData():
start_data_acquisition_event.wait()
while start_data_acquisition_event.is_set():
# time.sleep() simulates the time it takes the connection to fill up the buffer
time.sleep(4)
# send updateGuiFigure to tkinters event queue, so that it won't freeze
root.after(0, updateGuiFigure)
# *** set new data points on existing plot & blit GUI canvas ***
def updateGuiFigure():
# simulate data -> 15.000.000 points in real application
line.set_xdata(np.random.rand(10))
#
line.set_ydata(np.random.rand(10))
#
plotting_canvas.restore_region(background) # restore background
ax.draw_artist(line) # redraw just the line -> draw_artist updates axis
plotting_canvas.blit(ax.bbox) # fill in the axes rectangle
#
# *** update background for resize events ***
def update_background(event):
global background
background = plotting_canvas.copy_from_bbox(ax.bbox)
##########################*** MAIN ***#########################################################
# Init GUI
root = tkinter.Tk()
# Init frame & canvas
frame = ttk.Frame(root)
plotting_area = tkinter.Canvas(root, width=700, height=400)
#
frame.grid(row=0, column=1, sticky="n")
plotting_area.grid(row=0, column=0)
# Init button & bind to function onStartButtonClick
start_button = tkinter.Button(frame, text="Start")
start_button.bind("<Button-1>", onStartButtonClick)
start_button.grid(row=0, column=0)
# Init figure & axis
fig = Figure(figsize=(7, 4), dpi=100)
ax = fig.add_subplot(111)
# Connect figure to plotting_area from GUI
plotting_canvas = FigureCanvasTkAgg(fig, master=plotting_area)
# Set axis
ax.set_title('Test')
ax.grid(True)
ax.set_xlabel('x-axis')
ax.set_ylabel('y-axis')
ax.set(xlim=[0,1], ylim=[0, 1])
# Init plot
line, = ax.plot([], [])
# if animated == True: artist (= line) will only be drawn when manually called draw_artist(line)
line.set_animated(True)
# Draw plot to GUI canvas
plotting_canvas.draw()
plotting_canvas.get_tk_widget().pack(fill=tkinter.BOTH)
background = plotting_canvas.copy_from_bbox(ax.bbox) # cache background
plotting_canvas.mpl_connect('draw_event', update_background) # update background with 'draw_event'
# Init threads
start_data_acquisition_event = threading.Event()
#
init_settings_thread = threading.Thread(name='init_settings_thread', target=initSettings, daemon=True)
acquire_and_plot_data_thread = threading.Thread(name='acquire_and_plot_data_thread', target=acquireAndPlotData, daemon=True)
# Start tkinter mainloop
root.mainloop()
使用多个 类 处理的代码片段示例如下所示(与上面的代码相同,但不可重现,可以忽略):
def onStartButtonClick(self):
#
.
# Disable buttons and get widget values here etc.
.
#
self.start_data_acquisition_event = threading.Event()
self.init_settings_thread = threading.Thread(target=self.initSettings)
self.acquire_and_plot_data_thread = threading.Thread(target=self.acquireAndPlotData)
#
self.init_settings_thread.start()
self.acquire_and_plot_data_thread.start()
# FUNCTION END
def initSettings(self):
self.data_handler.setInitSettings(self.user_settings_dict)
self.data_handler.initDataAcquisitionObject()
self.start_data_acquisition_event.set()
def acquireAndPlotData(self):
self.start_data_acquisition_event.wait()
while self.start_data_acquisition_event.is_set():
self.data_handler.getDataFromDataAcquisitionObject()
self.master.after(0, self.data_plotter.updateGuiFigure)
所以我这样做了,但我不知道它是否适合你,或者这是否是一个好方法,但它可以保护你 .after
正如评论中所述,它有好处是你的函数 do_stuff
只在需要时被调用。
import tkinter as tk
import time
import threading
def get_data():
time.sleep(3)
print('sleeped 3')
_check.set(1)
def do_stuff():
try:
root.configure(bg='#'+str(_var.get()))
_var.set(_var.get()+101010)
except:
_var.set(101010)
root = tk.Tk()
_check = tk.IntVar(value=0)
_var = tk.IntVar(value=101010)
def callback(event=None, *args):
t1 = threading.Thread(target=get_data)
t1.start()
do_stuff()
_check.trace_add('write', callback) #kepp track of that variable and trigger callback if changed
callback() # start the loop
root.mainloop()
一些研究:
interpreter is only valid in the thread that created it, and all Tk activity must happen in this thread, also. That means that the mainloop must be invoked in the thread that created the interpreter. Invoking commands from other threads is possible; _tkinter will queue an event for the interpreter thread, which will then execute the command and pass back the result.
#l1493 var_invoke
The current thread is not the interpreter thread. Marshal
the call to the interpreter thread, then wait for
completion. */
if (!WaitForMainloop(self))
return NULL;
is-it-safe-to-use-a-intvar-doublevar-in-a-python-thread
When you set a variable, it calls the globalsetvar method on the master widget associated with the Variable. The _tk.globalsetvar method is implemented in C, and internally calls var_invoke, which internally calls WaitForMainLoop, which will attempt schedule the command for execution in the main thread, as described in the quote from the _tkinter source I included above.
Start
|
|<----------------------------------------------------------+
v ^
Do I have No[*] Calculate how Sleep for at |
work to do? -----> long I may sleep -----> most that much --->|
| time |
| Yes |
| |
v |
Do one callback |
| |
+-----------------------------------------------------------+
常识
来自 bugtracker:
Tkinter and threads.
If you want to use both tkinter and threads, the safest method is to make all tkinter calls in the main thread. If worker threads generate data needed for tkinter calls, use a queue.Queue to send the data to the main thread. For a clean shutdown, add a method to wait for threads to stop and have it called when the window close button [X] is pressed.
Just run all UI code in the main thread, and let the writers write to a Queue object; e.g.
结论
你做事的方式和我做事的方式看起来很像 ideal,但它们似乎并没有错。这取决于需要什么。