Python: многопоточность и tkinter

Я пытаюсь постоянно обновлять matlibplots в графическом интерфейсе tkinter, имея возможность нажимать кнопки, чтобы приостановить/продолжить/остановить обновление графиков. Я пытался использовать потоки, но они, похоже, не выполняются параллельно (например, поток данных выполняется, но графики не обновляются + нажатие кнопок игнорируется). Почему это не работает?

# Import Modules
import tkinter as tk
from threading import *
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
from scipy.fft import fft
import numpy as np
import time
import random

# global variables
state = 1             # 0 starting state; 1 streaming; 2 pause; -1 end and save
x = [0]*12
y = [0]*12

# Thread buttons and plots separately
def threading():
    state = 1
    
    t_buttons = Thread(target = buttons)
    t_plots = Thread(target = plots)
    t_data = Thread(target = data)
    
    t_buttons.start()
    t_plots.start()
    t_data.start()
    
def hex_to_dec(x, y):
    for i in range(0, 12):
        for j in range(0, len(y)):
            x[i][j] = int(str(x[i][j]), 16)
            y[i][j] = int(str(y[i][j]), 16)
    
def data():
    fig1, axs1 = main_plot()
    fig2, axs2 = FFT_plot()
    # To be replaced with actual Arduino data
    while(state!=-1):
        for i in range(0, 12):
            x[i] = [j for j in range(101)]
            y[i] = [random.randint(0, 10) for j in range(-50, 51)]
        for i in range(0, 12):
            for j in range(0, len(y)):
                x[i][j] = int(str(x[i][j]), 16)
                y[i][j] = int(str(y[i][j]), 16)

# create buttons
def stream_clicked():
    state = 1
    print("clicked")
    
def pause_clicked():
    state = 2
    print("state")
    
def finish_clicked():
    state = -1
    
    
def buttons():
    continue_button = tk.Button(window, width = 30, text = "Stream data" , 
                              fg = "black", bg = '#98FB98', command = stream_clicked)
    continue_button.place(x = window.winfo_screenwidth()*0.2, y = 0)

    pause_button = tk.Button(window, width = 30, text = "Pause streaming data" , 
                             fg = "black", bg = '#FFA000', command = pause_clicked)
    pause_button.place(x = window.winfo_screenwidth()*0.4, y = 0)

    finish_button = tk.Button(window, width = 30, text = "End session and save", 
                              fg = 'black', bg = '#FF4500', command = finish_clicked())
    finish_button.place(x = window.winfo_screenwidth()*0.6, y = 0)
    
def plots():
    fig1, axs1 = main_plot()
    fig2, axs2 = FFT_plot()
    
    if state==1:
        print("update")
        for i in range(0, 12):
            axs1[i].plot(x[i], y[i], 'blue')
            axs1[i].axes.get_yaxis().set_ticks([0], labels = ["channel  " + str(i+1)])
            axs1[i].grid(True)
            axs1[i].margins(x = 0)
        
        fig1.canvas.draw()
        fig1.canvas.flush_events()
        for i in range(0, 12):
            axs1[i].clear()
        for i in range(0, 12):
            axs2.plot(x[i], fft(y[i]))
        plt.title("FFT of all 12 channels", x = 0.5, y = 1)
        
        fig2.canvas.draw()
        fig2.canvas.flush_events()
        axs2.clear()

def main_plot():
    plt.ion()
    
    fig1, axs1 = plt.subplots(12, figsize = (10, 9), sharex = True)
    fig1.subplots_adjust(hspace = 0)
    # Add fixed values for axis
    
    canvas = FigureCanvasTkAgg(fig1, master = window)  
    canvas.draw()
    canvas.get_tk_widget().pack()
    canvas.get_tk_widget().place(x = 0, y = 35)
    
    return fig1, axs1
    
def update_main_plot(fig1, axs1):
    if state==1:
        for i in range(0, 12):
            axs1[i].plot(x[i], y[i], 'blue')
            axs1[i].axes.get_yaxis().set_ticks([0], labels = ["channel  " + str(i+1)])
            axs1[i].grid(True)
            axs1[i].margins(x = 0)
        axs1[0].set_title("Plot recordings", x = 0.5, y = 1)
        
        fig1.canvas.draw()
        fig1.canvas.flush_events()
        for i in range(0, 12):
            axs1[i].clear()
    
    
def FFT_plot():
    # Plot FFT figure 
    plt.ion()
    
    fig2, axs2 = plt.subplots(1, figsize = (7, 9))
    # Add fixed values for axis
    
    canvas = FigureCanvasTkAgg(fig2, master = window)  
    canvas.draw()
    canvas.get_tk_widget().pack()
    canvas.get_tk_widget().place(x = window.winfo_screenwidth()*0.55, y = 35)
    
    return fig2, axs2


def update_FFT_plot(fig2, axs2):
    # Update FFT plot
    for i in range(0, 12):
        axs2.plot(x[i], fft(y[i]))
    plt.title("FFT", x = 0.5, y = 1)
    
    fig2.canvas.draw()
    fig2.canvas.flush_events()
    axs2.clear()

# create root window and set its properties
window = tk.Tk()
window.title("Data Displayer")
window.geometry("%dx%d" % (window.winfo_screenwidth(), window.winfo_screenheight()))
window.configure(background = 'white')

threading()

window.mainloop()

*** Иногда это просто не работает без каких-либо сообщений, а иногда я также получаю «RuntimeError: основной поток не находится в основном цикле» ***

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

Community 29.10.2022 14:46

Проверьте расписание tkinter

mnikley 29.10.2022 14:49
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
2
73
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Честно говоря, все функции в вашем коде, скорее всего, вызовут ошибку сегментации, а другие функции, которые не приводят к ошибке сегментации, просто не работают, трудно объяснить, что не так.

  1. определите глобальные переменные как global, если вы собираетесь их изменять
  2. обновите графический интерфейс в своем основном потоке, многократно используя метод window.after.
  3. только чтение с вашего микроконтроллера должно выполняться в отдельном потоке.
  4. создание объектов Tkinter должно выполняться в основном потоке, в других потоках разрешены только обновления, но это не потокобезопасно, поэтому, хотя это может работать, иногда это может приводить к странному поведению или ошибкам.
  5. вызов функций matplotlib, таких как ion и flush_events, вызывает ошибки, потому что они предназначены для интерактивного холста matplotlib, а не для холста tkinter.
  6. многопоточность имеет очень сложную кривую обучения, поэтому спросите себя: «действительно ли мне нужны здесь потоки» и «есть ли способ не использовать потоки», прежде чем пытаться их использовать, поскольку, как только вы начнете использовать потоки, вы больше не используете «безопасный» Python. код», несмотря на все усилия, потоки небезопасны для любой задачи, вы должны сделать их безопасными, и, честно говоря, потоки здесь не нужны, если только вы не читаете 1 ГБ/с с вашего микроконтроллера.
  7. не используйте числа для состояний, это не pythonic, и это сбивает с толку читателей, и это не имеет преимущества в производительности по сравнению с использованием Enums.
  8. программы создаются постепенно, а не копируются и вставляются из нескольких рабочих фрагментов, поскольку труднее отследить, откуда возникает ошибка, когда несколько частей кода не были проверены на работоспособность.
# Import Modules
import tkinter as tk
from threading import *
import matplotlib.pyplot as plt
from matplotlib.backends.backend_tkagg import (FigureCanvasTkAgg, NavigationToolbar2Tk)
from scipy.fft import fft
import numpy as np
import time
import random
from enum import Enum,auto

UPDATE_INTERVAL_MS = 300

class States(Enum):
    STREAM = auto()
    PAUSE = auto()
    SAVE = auto()
    START = auto()


# global variables
state = States.START  # check States Enum
x = [[0]]*12
y = [[0]]*12


# Thread buttons and plots separately
def threading():
    global state
    global window
    state = States.STREAM

    buttons()
    plots()
    data()
    t_grab_data = Thread(target=grab_data_loop,daemon=True)
    t_grab_data.start()
    # t_buttons = Thread(target=buttons)
    # t_plots = Thread(target=plots)
    # t_data = Thread(target=data)
    #
    # t_buttons.start()
    # t_plots.start()
    # t_data.start()


def hex_to_dec(x, y):
    for i in range(0, 12):
        for j in range(0, len(y)):
            x[i][j] = int(str(x[i][j]), 16)
            y[i][j] = int(str(y[i][j]), 16)


def data():
    global fig1,axs1,fig2,axs2
    fig1, axs1 = main_plot()
    fig2, axs2 = FFT_plot()
    # To be replaced with actual Arduino data
    window.after(UPDATE_INTERVAL_MS,draw_data_loop)


def grab_data_loop():
    while state != States.SAVE:
        for i in range(0, 12):
            x[i] = [j for j in range(101)]
            y[i] = [random.randint(0, 10) for j in range(-50, 51)]
        for i in range(0, 12):
            for j in range(0, len(y)):
                x[i][j] = int(str(x[i][j]), 16)
                y[i][j] = int(str(y[i][j]), 16)
        time.sleep(0.1)  # because we are not reading from a microcontroller

def draw_data_loop():
    if state == States.STREAM:
        update_main_plot(fig1, axs1)
        update_FFT_plot(fig2, axs2)
    window.after(UPDATE_INTERVAL_MS,draw_data_loop)


# create buttons
def stream_clicked():
    global state
    state = States.STREAM
    print("clicked")


def pause_clicked():
    global state
    state = States.PAUSE
    print("state")


def finish_clicked():
    global state
    state = States.SAVE
    window.destroy()


def buttons():
    continue_button = tk.Button(window, width=30, text = "Stream data",
                                fg = "black", bg='#98FB98', command=stream_clicked)
    continue_button.place(x=window.winfo_screenwidth() * 0.2, y=0)

    pause_button = tk.Button(window, width=30, text = "Pause streaming data",
                             fg = "black", bg='#FFA000', command=pause_clicked)
    pause_button.place(x=window.winfo_screenwidth() * 0.4, y=0)

    finish_button = tk.Button(window, width=30, text = "End session and save",
                              fg='black', bg='#FF4500', command=finish_clicked)
    finish_button.place(x=window.winfo_screenwidth() * 0.6, y=0)


def plots():
    global state
    fig1, axs1 = main_plot()
    fig2, axs2 = FFT_plot()

    if state == States.STREAM:
        print("update")
        for i in range(0, 12):
            axs1[i].plot(x[i], y[i], 'blue')
            axs1[i].axes.get_yaxis().set_ticks([0], labels=["channel  " + str(i + 1)])
            axs1[i].grid(True)
            axs1[i].margins(x=0)

        # fig1.canvas.draw()
        # fig1.canvas.flush_events()
        # for i in range(0, 12):
        #     axs1[i].clear()
        for i in range(0, 12):
            axs2.plot(x[i], np.abs(fft(y[i])))
        plt.title("FFT of all 12 channels", x=0.5, y=1)

        # fig2.canvas.draw()
        # fig2.canvas.flush_events()
        # axs2.clear()


def main_plot():
    # plt.ion()
    global canvas1
    fig1, axs1 = plt.subplots(12, figsize=(10, 9), sharex=True)
    fig1.subplots_adjust(hspace=0)
    # Add fixed values for axis

    canvas1 = FigureCanvasTkAgg(fig1, master=window)
    # canvas.draw()
    canvas1.get_tk_widget().pack()
    canvas1.get_tk_widget().place(x=0, y=35)

    return fig1, axs1


def update_main_plot(fig1, axs1):
    if state == States.STREAM:
        for i in range(0, 12):
            axs1[i].clear()
        for i in range(0, 12):
            axs1[i].plot(x[i], y[i], 'blue')
            axs1[i].axes.get_yaxis().set_ticks([0], labels=["channel  " + str(i + 1)])
            axs1[i].grid(True)
            axs1[i].margins(x=0)
        axs1[0].set_title("Plot recordings", x=0.5, y=1)
        canvas1.draw()
        # fig1.canvas.draw()
        # fig1.canvas.flush_events()



def FFT_plot():
    # Plot FFT figure
    # plt.ion()
    global canvas2
    fig2, axs2 = plt.subplots(1, figsize=(7, 9))
    # Add fixed values for axis

    canvas2 = FigureCanvasTkAgg(fig2, master=window)
    # canvas.draw()
    canvas2.get_tk_widget().pack()
    canvas2.get_tk_widget().place(x=window.winfo_screenwidth() * 0.55, y=35)

    return fig2, axs2


def update_FFT_plot(fig2, axs2):
    # Update FFT plot
    if state == States.STREAM:
        axs2.clear()
        for i in range(0, 12):
            axs2.plot(x[i], np.abs(fft(y[i])))
        plt.title("FFT", x=0.5, y=1)
        canvas2.draw()
    # fig2.canvas.draw()
    # fig2.canvas.flush_events()
    # axs2.clear()


# create root window and set its properties
window = tk.Tk()
window.title("Data Displayer")
window.geometry("%dx%d" % (window.winfo_screenwidth(), window.winfo_screenheight()))
window.configure(background='white')

threading()

window.mainloop()

# put saving logic here

Такое подробное объяснение и решение много значат, большое спасибо, Ахмед!

AMcoding 29.10.2022 17:36

Привет, вопрос как нуб Python: почему вы использовали многопоточность, но не использовали IPC (межпроцессное взаимодействие) по сравнению с глобальными переменными?

Custos Mortem 31.10.2022 18:22

@CustosMortem, использующий многопроцессорность и IPC, требует, чтобы ядра выполняли дополнительную работу по сериализации и десериализации данных между процессами, в этих накладных расходах нет необходимости, особенно когда чтение с микроконтроллера сбрасывает GIL, поэтому вы можете иметь 2 потока, работающих одновременно, это быстрее, потому что им не нужно тратить циклы на IPC. более чистым подходом было бы использование потокобезопасной очереди, для этого не потребуется сериализация, поскольку она находится в одном процессе, и вы не будете читать из глобальной переменной, поэтому она более масштабируема для нескольких устройств одновременно. время.

Ahmed AEK 31.10.2022 18:32

@CustosMortem прочитайте это, чтобы узнать, когда использовать каждый многопроцессорность против многопоточности против асинхронности в Python 3, единственное, что я бы добавил к этому, это то, что чтение из последовательных портов / USB / dll сбрасывает GIL, поэтому лучше использовать потоки при работе с драйверами.

Ahmed AEK 31.10.2022 18:36

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