【问题标题】:Tkinter event_generate freezes thread joinTkinter event_generate 冻结线程连接
【发布时间】:2021-11-03 11:41:26
【问题描述】:

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

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

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

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

有没有更优雅的方式让我调用 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 上测试

【问题讨论】:

    标签: python multithreading tkinter events deadlock


    【解决方案1】:

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

    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 它本身不是线程安全的。尝试this 示例。即使没有从另一个线程调用 tkinter 方法,它也会使 python 解释器崩溃。在某些情况下,仅将 tkinter 小部件/变量传递给另一个线程可能会导致问题。

    【讨论】:

    • 嗨 TheLizzard,感谢您的帮助。我认为您的建议正是我提到的解决方法 2。这并不安全。 must_stop.set()self.thread.join() 也可以在查询 must_stop.is_set() 后直接调用,从而导致死锁。但是,可以通过使用带锁的条件来解决,该锁在每个 event_generate() 之前都经过验证。不过,我的想法是必须有更好的解决方案来解决这个问题,我想知道是否有解决这个线程问题的标准方法。
    • @rhb 我的错。我忘了你提到过。同样使用我拥有的代码,如果您担心性能,可以将while not must_stop.is_set() 更改为while True
    • 顺便说一句,你的错误示例并没有吓到我太多:-) 因为它可以通过在退出主循环之前关闭所有线程来解决。这就是我正在做的事情。但我需要在我的示例中加入 - 或者等待 not is_alive() - 因为在我的真实应用程序中,稍后将启动更多线程,必须确保其他线程已完成。
    • @rhb 我建议将线程放在一个列表中并在.mainloop() 之后对它们调用.join()。它应该停止我谈到的问题
    • 您对 tkinter 小部件/变量绝对正确,不能从外部修改。我的解决方案是使用线程安全队列和事件来通知更新,然后修改 tk.Variables。到目前为止,这对我来说效果很好,我没有收到 tkinter 关于“主线程不在主循环中”的任何抱怨
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-10-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-04-08
    • 1970-01-01
    相关资源
    最近更新 更多