Графический интерфейс Tkinter, ввод-вывод и потоки: когда использовать очереди, когда события?

Я использую TKinter для создания графического интерфейса (для подключения сокета к многоканальному анализатору) для получения и построения данных (~15000000 значений) через равные промежутки времени (~15 секунд).

При получении данных я не хочу, чтобы графический интерфейс зависал, поэтому я использую многопоточность для обработки соединений, приема данных и операций построения графиков. Я сделал это, как видно из воспроизводимого кода, установив событие с помощьюthreading.Event() и обрабатывать один поток за другим (несколько строк кода в initSettings() & acquireAndPlotData). Единственный раз, когда я вмешиваюсь в графический интерфейс, - это когда я рисую на холсте, и я делаю это с помощью tkintersafter() метод.

При запуске код работает без зависания, получает и строит график, пока окно открыто и работает должным образом.

Когда я читал об обработке блокирующих операций ввода-вывода в графическом интерфейсе tkinter, я нашел только примеры с постановкой в ​​очередь и рекурсивной проверкой очереди (с Queue & after(),12345), но я обнаружил, что эти операции намного удобнее и проще выполнять с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)

1 ответ

Решение

Итак, я сделал это так, но я не знаю, подходит ли он вам или это хороший способ сделать это, но он защищает вас от .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()

Некоторые исследования:

[Tcl]

Интерпретатор действителен только в потоке, который его создал, и все действия Tk также должны происходить в этом потоке. Это означает, что основной цикл должен быть вызван в потоке, создавшем интерпретатор. Возможен вызов команд из других потоков; _tkinter поставит событие в очередь для потока интерпретатора, который затем выполнит команду и вернет результат.

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

безопасно ли использовать-intvar-doublevar-in-a-python-thread

Когда вы устанавливаете переменную, он вызывает метод globalsetvar на главном виджете, связанном с переменной. Метод _tk.globalsetvar реализован на C и внутренне вызывает var_invoke, который внутренне вызывает WaitForMainLoop, который пытается запланировать выполнение команды в основном потоке, как описано в цитате из источника _tkinter, который я включил выше.

wiki.tcl

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

Здравый смысл

из багтрекера:

Tkinter и потоки.

Если вы хотите использовать как tkinter, так и потоки, самый безопасный метод - выполнять все вызовы tkinter в основном потоке. Если рабочие потоки генерируют данные, необходимые для вызовов tkinter, используйте queue.Queue для отправки данных в основной поток. Для чистого завершения работы добавьте метод ожидания остановки потоков и его вызова при нажатии кнопки закрытия окна [X].

эффбот

Просто запустите весь код пользовательского интерфейса в основном потоке и позвольте авторам писать в объект Queue; например

Вывод

То, как вы это сделали, и то, как я это сделал, кажется идеальным, но они не кажутся неправильными. Это зависит от того, что нужно.

Другие вопросы по тегам