【Bug已解决】Windows Codex Desktop 26.623.13972.0 freezes when clicking Add file button 解决方案原始报错Windows Codex Desktop 26.623.13972.0 freezes when clicking Add file button 场景在 Windows 桌面应用里点添加文件按钮整个界面卡死鼠标转圈、点哪都没反应要等很久才恢复有时直接无响应。版本 26.623.13972.0。 关键词UI 冻结、主线程阻塞、文件对话框、后台线程、事件循环、异步加载。一、现象长什么样点击添加文件按钮后界面立刻失去响应——按钮不弹起、窗口不刷新、菜单点不开等十几秒甚至更久才恢复期间 CPU 可能不高说明不是死算是在等 IO/对话框恢复后文件对话框才出现或才把文件加进来在配置差/网络盘/大目录下更明显暗示和列目录/打开对话框的耗时有关。这是典型的主线程UI 线程被阻塞点击按钮的处理函数里同步做了本应异步的事打开系统文件对话框、遍历目录、读文件头UI 事件循环在那段时间无法处理任何消息于是整窗冻结。二、背景为什么点按钮会冻住整个界面桌面应用的 UI 靠一个事件循环消息泵驱动点击、绘制、输入都排队在这个循环里处理。这个循环跑在主线程上。一旦按钮的点击事件处理函数里执行了耗时同步操作比如open_file_dialog()内部同步枚举网络盘目录、或同步读取大文件主线程就被占住事件循环停转界面自然冻结。文件对话框尤其容易踩坑某些系统/框架的文件对话框在显示前会同步枚举初始目录内容若初始目录是缓慢的网络位置或海量目录就会卡在主线程。三、根因耗时操作跑在主线程根因拆解同步打开对话框点击 handler 直接调show_file_dialog()内部同步列目录主线程阻塞。同步扫描目录选定后 handler 同步递归扫描文件、读元数据耗时全在主线程。无异步没有把打开对话框 处理文件放到后台线程或异步任务。无超时/取消即使慢也没法取消只能干等。初始目录不当对话框默认打开到一个很慢的位置网络盘/根目录放大阻塞。下面用最小模型复现主线程同步阻塞导致事件循环停转再给修复。四、最小可运行复现用一个简化的事件循环模拟。错误写法点击处理里同步 sleep代表同步 IO循环被卡住。import time class EventLoop: def __init__(self): self.queue [] def post(self, task): self.queue.append(task) def run(self): # 简化的 UI 事件循环一次处理一个事件 while self.queue: task self.queue.pop(0) task() class App: def __init__(self, loop: EventLoop): self.loop loop def on_add_file_click(self): # 错误在主线程同步做耗时操作模拟打开对话框扫描 time.sleep(3) # 阻塞 3 秒 self.loop.post(lambda: print(文件已添加)) def on_paint(self): print([UI] 重绘) if __name__ __main__: loop EventLoop() app App(loop) loop.post(app.on_paint) loop.post(app.on_add_file_click) # 点击阻塞 3 秒期间 paint 排不上 loop.post(app.on_paint) loop.run() # 注意on_add_file_click 的 3s sleep 在主线程后续事件被拖延真实 GUI 里这就是点按钮后整窗卡 3 秒的复现——事件循环被一个同步耗时任务堵住。五、方案文件对话框放后台线程第一层点击按钮只发起打开文件对话框的请求真正的对话框/目录枚举放到后台线程结果通过回调/队列回主线程import threading, queue class AppFixed: def __init__(self): self.result_q queue.Queue() def on_add_file_click(self): # 主线程只负责派发不阻塞 t threading.Thread(targetself._open_dialog_worker, daemonTrue) t.start() def _open_dialog_worker(self): # 后台线程打开对话框 枚举耗时操作离主线程 time.sleep(3) # 模拟慢对话框/扫描 chosen /path/to/file.txt self.result_q.put(chosen) # 结果回主线程 def pump_results(self): # 主线程事件循环里定期取结果不阻塞 try: chosen self.result_q.get_nowait() print(文件已添加:, chosen) except queue.Empty: pass if __name__ __main__: app AppFixed() app.on_add_file_click() # 立即返回UI 不卡 for _ in range(5): time.sleep(0.1) app.pump_results() # 主线程照常处理其他事件点击瞬间返回耗时操作在后台跑UI 始终能响应结果回来再处理。六、方案异步加载与目录扫描第二层即便拿到文件后续处理扫描、读头、缩略图也异步化并用生成器分批避免一次占满主线程import os def scan_files_async(root, on_batch): batch [] for entry in os.scandir(root): batch.append(entry.name) if len(batch) 50: # 每 50 个一批回传 on_batch(batch) batch [] if batch: on_batch(batch) class AsyncLoader: def __init__(self): self.collected [] def load(self, root): # 后台线程跑扫描分批回调主线程 def worker(): scan_files_async(root, self._on_batch) threading.Thread(targetworker, daemonTrue).start() def _on_batch(self, batch): self.collected.extend(batch) print(f[主线程] 已收到 {len(batch)} 个累计 {len(self.collected)}) if __name__ __main__: loader AsyncLoader() loader.load(.) # 扫描在后台主线程按批收结果不冻结 time.sleep(0.2)分批回传让主线程每次只处理一小撮事件循环始终有空处理 UI 消息。七、方案可取消 超时避免无限等待第三层耗时操作应可取消、有超时用户不必干等class CancellableScan: def __init__(self): self._cancel False def request_cancel(self): self._cancel True def run(self, items, on_progress): for i, it in enumerate(items): if self._cancel: on_progress(已取消) return time.sleep(0.01) on_progress(f处理 {i1}/{len(items)}) if __name__ __main__: cs CancellableScan() cs.run(list(range(1000)), print) cs.request_cancel() # 用户随时可取消不必等完取消/超时机制把卡死等很久变成随时可中断体验从冻结转为可控。八、验证把点击不阻塞主线程锁进测试def test_click_does_not_block_event_loop(): loop EventLoop() appf AppFixed() # 点击只派发线程不 sleep 在主线程 appf.on_add_file_click() # 主线程立刻还能处理其他事件不卡 processed [] loop.post(lambda: processed.append(paint)) loop.run() assert paint in processed def test_scan_batches_do_not_hog_main_thread(): loader AsyncLoader() loader.load(.) time.sleep(0.2) assert len(loader.collected) 0 # 至少不崩、不阻塞 if __name__ __main__: test_click_does_not_block_event_loop() test_scan_batches_do_not_hog_main_thread() print(添加文件不冻结测试通过。)九、排查清单点按钮界面冻结按顺序查主线程点击 handler 里有没有同步耗时操作打开对话框、扫描、读文件对话框文件对话框是否在主线程同步打开其初始目录是否很慢网络盘/大目录异步化耗时操作是否放到后台线程 / 异步任务主线程只派发与收结果分批大目录扫描/处理是否分批回传而非一次占满主线程事件循环冻结期间事件循环是否停转点击/绘制都不响应是则确认主线程被占。取消/超时慢操作能否取消或超时用户是否只能干等平台差异是否只在 Windows 明显可能 Windows 文件对话框默认枚举更慢。十、小结点添加文件按钮冻结是点击处理函数在主线程同步执行了本应异步的耗时操作打开文件对话框、枚举/扫描目录占住 UI 事件循环导致整窗无响应。修复三层移出主线程对话框/扫描放到后台线程主线程只派发和收结果分批异步大目录处理按批回传主线程事件循环始终有空处理 UI可取消超时慢操作支持中断用户不必干等。核心原则UI 主线程永远只做快进快出的事。任何可能耗时的 IO、对话框、扫描都必须在后台线程或异步任务里完成结果通过队列/回调回主线程——这是桌面应用不冻结的底线。