首页 > 解决方案 > Tkinter event_generate 冻结线程连接

问题描述

我有一个运行 CPU 密集型任务的 tkinter 应用程序。因此,为了保持 GUI 响应,我将任务放在它们自己的线程中并通过事件与 GUI 通信。这很好用,据我所知,只要我不直接从工作线程操作 tkinter 小部件和变量,它就应该是安全的。事实上,它正在工作。但是当我需要停止一个工作任务时,我命令工作线程停止一个名为must_stop的threading.Event并从 GUI 调用 join 。但是,当工作线程在意识到必须停止之前再生成一个事件时,连接会冻结。这很烦人。

我找到了一些避免冻结的方法,使代码有些难看。我可以:

  1. 在 tkinter 窗口上调用update()时使用循环检查thread.is_alive()。不确定这是否会破坏我的主循环
  2. 在每个event_generate调用之前检查must_stop是否已设置(或者在调用 join 之前使用 GUI 应用的threading.Lockthreading.Condition可能更好)

我在下面放了一个简短的工作示例。顺便说一句:如果我使用event_generate来产生一个事件或例如用于 GUI 的tk.IntVar(跟踪 var 或设置标签的文本变量- 即使它根本没有连接,它也会导致加入期间死锁)

有没有更优雅的方式让我在没有死锁的情况下调用thread.join() ?还是与 tkinter GUI 进行通信的更好概念?据我所知,tkinter 事件被称为“线程安全”。

import threading
import tkinter as tk
import time

must_stop = threading.Event()
counter_lock = threading.Lock()
counter = 0

def run_thread(root):
    #Threaded procedure counting seconds and generating events for the root window whenever
    #the variable changes
    global counter
    while not must_stop.is_set():
        time.sleep(1)
        with counter_lock:
            counter += 1
        root.event_generate('<<Counter>>', when = 'tail')

class CounterWindow(tk.Tk):
    #Window class for the counter
    def __init__(self):
        super().__init__()
        self.label = tk.Label(self, text = 'Hello!')
        self.label.pack()
        self.button = tk.Button(text = 'Start counter', command = self.start_thread)
        self.button.pack()
        self.bind('<<Counter>>', self.update_counter)
        
    def update_counter(self, event):
        #Writes counter to label, triggered by <<Counter>> event
        with counter_lock:
            self.label.configure(text = counter)   # replacing this line
            #print(counter)                                 # with a tk-free routine does not prevent deadlock
            
    def start_thread(self):
        #Button command to start the thread
        self.thread = threading.Thread(target = run_thread, args = (self, ))
        self.thread.start()
        self.button.configure(text = 'Stop counter', command = self.stop_thread)
        
    def stop_thread(self):
        #Button command to stop the thread. Attention: Causing deadlock !!!
        #self.unbind('<<Counter>>')    # does not prevent deadlock either
        must_stop.set()
        self.thread.join()                                    # replacing this line
        #while self.thread.is_alive():                  # with this loop prevents deadlock
        #    self.update()
        self.button.configure(text = 'Exit counter', command = self.destroy)
        
#Start the app
window = CounterWindow()
window.mainloop()

使用 python 版本 3.9.5。在 Windows 和 linux 上测试

标签: pythonmultithreadingtkintereventsdeadlock

解决方案


这不会给我任何错误并且运行良好:

import threading
import tkinter as tk
import time

must_stop = threading.Event()
counter_lock = threading.Lock()
counter = 0

def run_thread(root):
    global counter
    while not must_stop.is_set():
        time.sleep(1)
        with counter_lock:
            counter += 1
        if must_stop.is_set():
            return None
        root.event_generate('<<Counter>>', when="tail")


class CounterWindow(tk.Tk):
    # Window class for the counter
    def __init__(self):
        super().__init__()
        self.label = tk.Label(self, text="Hello!")
        self.label.pack()
        self.button = tk.Button(text="Start counter", command=self.start_thread)
        self.button.pack()
        self.bind("<<Counter>>", self.update_counter)
        
    def update_counter(self, event):
        #Writes counter to label, triggered by <<Counter>> event
        with counter_lock:
            self.label.configure(text=counter)
            
    def start_thread(self):
        #Button command to start the thread
        self.thread = threading.Thread(target=run_thread, args=(self, ))
        self.thread.start()
        self.button.configure(text="Stop counter", command=self.stop_thread)
        
    def stop_thread(self):
        must_stop.set()
        # self.thread.join() # Don't think it's nessasary
        self.button.configure(text="Exit counter", command=self.destroy)
        
# Start the app
window = CounterWindow()
window.mainloop()

我在调用之前添加了另一个检查,.event_generate()以便在must_stop设置时不会生成事件。我也删除了.join(),我不是threading专家,但据我所知,这并不是真正必要的。

您的代码更大的问题是tkinter它本身不是线程安全的。试试这个例子。即使没有tkinter从另一个线程调用任何方法,它也会使 python 解释器崩溃。tkinter在某些情况下,仅将小部件/变量传递给另一个线程可能会导致麻烦。


推荐阅读