#python #multithreading
Вопрос:
Я пытаюсь запустить сценарий, который выполняется асинхронно с использованием потоков. Я столкнулся с проблемой о том, как периодически проверять, жив ли поток (в разделе start_thread1). Я не хочу использовать join (), так как это заморозит графический интерфейс до тех пор, пока потоки не будут завершены.
Если это невозможно, я открыт для любых других способов сделать это.
Вот код, который я использую — это часть части кода, просто чтобы обрисовать «проблему», которая у меня есть:
from tkinter.constants import LEFT, RIGHT, S import tkinter.messagebox from matplotlib import pyplot as plt import tkinter as tk, time, threading, random, queue class GuiPart(object): def __init__(self, master, queue, queue2, client_instance): self.queue = queue self.queue2 = queue2 self.x = [] self.y= [] # Set up the GUI self.Button2 = tk.Button(master, text="Button2", padx=10, pady=5, fg="white", bg="#263D42", command=client_instance.start_thread1) self.Button2.pack(side = RIGHT) def processIncoming(self): """ Handle all messages currently in the queue, if any. """ while not self.queue.empty(): msg = self.queue.get_nowait() self.x.append(msg) print(msg) while not self.queue2.empty(): msg2 = self.queue2.get_nowait() self.y.append(msg2) fig, ax = plt.subplots() ax.plot(self.x, self.y) plt.show() class ThreadedClient(object): """ Launch the main part of the GUI and the worker thread. periodic_call() and end_application() could reside in the GUI part, but putting them here means that you have all the thread controls in a single place. """ def __init__(self, master): """ Start the GUI and the asynchronous threads. We are in the main (original) thread of the application, which will later be used by the GUI as well. We spawn a new thread for the worker (I/O). """ self.master = master # Create the queue self.queue = queue.Queue() self.queue2 = queue.Queue() self.running=True # Set up the GUI part self.gui = GuiPart(master, self.queue, self.queue2, self) # Set up the thread to do asynchronous I/O # More threads can also be created and used, if necessary def start_thread1(self): thread1=threading.Thread(target=self.worker_thread1) thread1.start() # how to check periodically if the thread is finished and when is finished run self.gui.processIncoming() # if I run it straight away like this the self.gui.processIncoming() will run before the thread will finish and nothing will be plotted if thread1.is_alive() == False: self.gui.processIncoming() def worker_thread1(self): """ This is where we handle the asynchronous I/O. For example, it may be a 'select()'. One important thing to remember is that the thread has to yield control pretty regularly, be it by select or otherwise. """ if self.running: time.sleep(5) # I am using time.sleep(5) just to simulate a long-running process for i in range(20): msg = i self.queue.put(msg) for j in range(20): msg2 = j self.queue2.put(msg2) root = tk.Tk() root.title('Matplotlib threading') client = ThreadedClient(root) root.mainloop()
Комментарии:
1. Можете ли вы установить дочернему потоку логическое значение True непосредственно перед его выходом? Затем основной поток может просто проверить, что логическое значение, если оно все еще является значением по умолчанию/False, дочерний поток (вероятно) все еще работает; если это правда, вы можете безопасно вызвать join (), не беспокоясь о замораживании графического интерфейса.
2. @JeremyFriesner Не могли бы вы предоставить минимальный пример кода? Я не могу себе представить, как это сделать.
3. Почему бы вам просто не вызвать thread.is_alive (), чтобы проверить, запущен ли поток?
4. @PaulCornelius Я обновил вопрос тем, что я пробовал. Если я это сделаю, «если» запускается сразу после thread1.start (), и будет отображаться только пустой участок. Оператор if не позволит потоку завершить работу.
5. @Tibi добавьте
self.running = False
в нижнюю часть вашегоworker_thread1(self)
метода. Затем всякий раз, когда вы хотите проверить,join()
вернется ли он быстро, вы можетеif (client.running == False)
проверить, завершен ли уже поток, и звонить толькоjoin()
в том случае, если он уже завершен.
Ответ №1:
Если я правильно понимаю, у вас есть программа tkinter со вторым потоком, которая выполняет некоторый сбор данных, и вам нужно получить данные из этого второго потока обратно в графический интерфейс. Вы не можете просто ждать завершения второго потока, потому что это заблокировало бы основной цикл tkinter.
Одним из решений является создание функции обратного вызова, передача ее во второй поток и вызов ее в качестве самого последнего шага во втором потоке. Большинство объектов tkinter не являются потокобезопасными, поэтому, если вы собираетесь обновить графический интерфейс в функции обратного вызова, вам придется выполнить обратный вызов в основном потоке. Чтобы сделать это, основывайте обратный вызов на after_idle
функции tkinter. Это приводит к тому, что обратный вызов происходит в цикле событий tk, в основном потоке, подобно обработчику событий tkinter.
Эта программа делает это и похожа на вашу программу. Я изменил несколько незначительных вещей, чтобы сделать мою проверку статического типа (pylint) счастливой. Я не использую matplotlib, поэтому я удалил этот код.
Самое важное уже внутри start_thread1
. Функция f объявляется и передается в качестве аргумента потоку. Обратите внимание, что f не вызывает processIncoming, а передает его в after_idle; это указывает основному циклу tk выполнить фактический вызов. Функция, переданная в worker_thread1, вызывается как последний шаг в потоке.
Конечным результатом является то, что processIncoming() запускается в основной поток, когда рабочий поток завершается.
from tkinter.constants import RIGHT import tkinter as tk import time import threading import queue class GuiPart: def __init__(self, master, queue1, queue2, client_instance): self.queue1 = queue1 self.queue2 = queue2 self.x = [] self.y= [] # Set up the GUI self.Button2 = tk.Button(master, text="Button2", padx=10, pady=5, fg="white", bg="#263D42", command=client_instance.start_thread1) self.Button2.pack(side = RIGHT) def processIncoming(self): """ Handle all messages currently in the queue, if any. """ print(threading.current_thread()) while not self.queue1.empty(): msg = self.queue1.get_nowait() self.x.append(msg) print("X", msg) while not self.queue2.empty(): msg2 = self.queue2.get_nowait() self.y.append(msg2) print("Y", msg2) print("Make a plot now") class ThreadedClient: """ Launch the main part of the GUI and the worker thread. periodic_call() and end_application() could reside in the GUI part, but putting them here means that you have all the thread controls in a single place. """ def __init__(self, master): """ Start the GUI and the asynchronous threads. We are in the main (original) thread of the application, which will later be used by the GUI as well. We spawn a new thread for the worker (I/O). """ self.master = master # Create the queue self.queue1 = queue.Queue() self.queue2 = queue.Queue() self.running=True # Set up the GUI part self.gui = GuiPart(master, self.queue1, self.queue2, self) # Set up the thread to do asynchronous I/O # More threads can also be created and used, if necessary def start_thread1(self): def f(): self.master.after_idle(self.gui.processIncoming) thread1=threading.Thread(target=self.worker_thread1, args=(f, )) thread1.start() def worker_thread1(self, callback): """ This is where we handle the asynchronous I/O. For example, it may be a 'select()'. One important thing to remember is that the thread has to yield control pretty regularly, be it by select or otherwise. """ print(threading.current_thread()) if self.running: time.sleep(1) # simulate a long-running process for i in range(20): msg = i self.queue1.put(msg) for j in range(20): msg2 = j self.queue2.put(msg2) callback() root = tk.Tk() root.title('Matplotlib threading') client = ThreadedClient(root) root.mainloop()