點擊「開始執行」按鈕後,整個 Tkinter 視窗無響應,直到測試用例跑完才恢復。代碼已將測試邏輯放入獨立執行緒 `exe_thread`,但仍凍結。
def ExcuteSet():
global Exe_Flag
Exe_Flag = True
exe_thread = threading.Thread(target=ExecuteCase)
exe_thread.setDaemon(True)
exe_thread.start()
exe_thread.join() # <=== 罪魁禍首!主執行緒在此阻塞等待子執行緒結束
Tkinter 的事件迴圈 (`root.mainloop()`) 運行在主執行緒。呼叫 `join()` 會導致主執行緒同步等待子執行緒結束,期間無法處理任何 GUI 事件 (重繪、滑鼠、鍵盤),視窗呈現「假死」。
import threading
import queue
import tkinter as tk
from tkinter import ttk
class App:
def __init__(self, root):
self.root = root
self.msg_queue = queue.Queue() # 執行緒安全佇列
self.build_ui()
self.poll_queue() # 定時輪詢佇列更新 UI
def build_ui(self):
self.btn_start = ttk.Button(root, text="開始執行", command=self.on_start)
self.btn_start.pack()
self.log = tk.Text(root)
self.log.pack()
def on_start(self):
self.btn_start.config(state=tk.DISABLED)
self.log.insert(tk.END, "開始執行...\n")
# 啟動工作執行緒,不 join!
threading.Thread(target=self.worker, daemon=True).start()
def worker(self):
# 模擬耗時任務
for i in range(5):
time.sleep(1)
self.msg_queue.put(f"步驟 {i+1} 完成\n")
self.msg_queue.put(("DONE", "全部完成\n"))
def poll_queue(self):
"""主執行緒定時檢查佇列,更新 UI"""
try:
while True:
msg = self.msg_queue.get_nowait()
if isinstance(msg, tuple) and msg[0] == "DONE":
self.log.insert(tk.END, msg[1])
self.btn_start.config(state=tk.NORMAL)
else:
self.log.insert(tk.END, msg)
self.log.see(tk.END)
except queue.Empty:
pass
self.root.after(100, self.poll_queue) # 100ms 輪詢一次
if __name__ == "__main__":
root = tk.Tk()
App(root)
root.mainloop()
| 錯誤做法 | 正確做法 | 說明 |
|---|---|---|
| `thread.join()` 在主執行緒 | 不 join,使用 `daemon=True` | 主執行緒必須自由跑 mainloop |
| 子執行緒直接操作 UI (`label.config()`) | 子執行緒放入 `queue.Queue`,主執行緒 `after` 輪詢取出更新 | Tkinter 非執行緒安全,所有 UI 操作必須在主執行緒 |
| 長時間任務阻塞主執行緒 | 拆分為小任務 + `after` 排程,或用 `concurrent.futures` + 回調 | 保持事件迴圈響應 |
from concurrent.futures import ThreadPoolExecutor
executor = ThreadPoolExecutor(max_workers=2)
def on_start(self):
future = executor.submit(self.long_task)
future.add_done_callback(lambda f: self.root.after(0, self.on_task_done, f))
def on_task_done(self, future):
try:
result = future.result()
self.log.insert(tk.END, f"結果: {result}\n")
except Exception as e:
self.log.insert(tk.END, f"錯誤: {e}\n")
self.btn_start.config(state=tk.NORMAL)
此模式自動處理執行緒池、異常傳播、回調排程到主執行緒,更現代健壯。