在PyGame GUI事件循环内启动线程为何拖慢GUI响应?
问题:PyGame GUI与并行测量线程共存时卡顿异常
在PyGame GUI循环内启动了一个可由用户启停的测量线程,线程运行正常,但GUI的绘制与元素响应严重卡顿,几乎失去响应。疑惑是否需要将GUI重绘放入单独线程?核心简化代码如下:
# 子线程执行的函数:测量时向队列放入随机数 def takeMeasurements(run, measure, q, sleep_time): while run.is_set(): if measure.is_set(): print("Measuring...") q.put(random()) else: print("Not measuring...") sleep(sleep_time) ######## PyGame 代码 ######## pygame.init() # 此处省略大量GUI初始化代码 #{....} run = True MEASURE = False # 事件与GUI更新循环 while run: time_delta = clock.tick(60)/1000.0 screen.fill((0,0,0)) for event in pygame.event.get(): if event.type == pygame.QUIT: run = False if event.type == pygame_gui.UI_BUTTON_PRESSED: if event.ui_element == button_set_param: # 用户点击启动测量按钮 if stdb_run == "Standby": pass elif stdb_run == "Run": if MEASURE == False: # 仅第一次启动时初始化线程 threadRun = Event() threadRun.set() threadMeasure = Event() threadMeasure.set() queue = Queue() # 启动线程,休眠1秒 t = Thread(target=takeMeasurements, args=[threadRun, threadMeasure, queue, 1]) t.start() else: # 非首次启动,直接恢复测量 threadMeasure.set() MEASURE = True if event.type == pygame_gui.UI_BUTTON_PRESSED: # 用户点击停止测量按钮 if event.ui_element == button_stop: threadMeasure.clear() try: # 尝试从队列获取测量数据并更新GUI m = queue.get() entry_vbridge.set_text(str(m)) except: pass # 更新并绘制GUI manager.update(time_delta) manager.draw_ui(screen) screen.blit(surf, (750,400)) pygame.display.flip() # 程序退出时清理线程 try: threadRun.clear() t.join() except NameError: threadRun = None t = None pygame.quit()
曾误以为takeMeasurements函数中的sleep()会影响GUI循环,虽知道thread.sleep()但未传入线程参数,实际并非此原因。
问题根源
GUI卡顿的核心原因是queue.get()的阻塞特性:该方法默认会一直等待,直到队列中有数据可用。当测量线程未产出数据时,主线程(GUI循环)会被挂起在这一行代码上,无法处理事件、更新和绘制GUI,导致界面失去响应。
另外,绝对不要将GUI重绘放入单独线程:PyGame(以及绝大多数GUI库)要求所有GUI操作(如更新控件、绘制界面)必须在主线程执行,子线程操作GUI会引发不可预测的错误甚至崩溃。
解决方案
修改队列数据的获取方式,使用非阻塞调用或设置超时,避免主线程被挂起:
方法1:使用get_nowait()非阻塞获取
from queue import Empty # 需要导入Empty异常 # 替换原有的try-except块 try: m = queue.get_nowait() # 无数据时直接抛出Empty异常 entry_vbridge.set_text(str(m)) except Empty: pass # 无数据时跳过,不阻塞主线程
方法2:给get()设置超时时间
from queue import Empty try: # 设置极短超时(如0.001秒),避免阻塞主线程 m = queue.get(timeout=0.001) entry_vbridge.set_text(str(m)) except Empty: pass
额外优化建议
- 将
queue、threadRun、threadMeasure、t这些线程相关变量移到全局作用域,避免局部变量可能引发的NameError - 线程启动逻辑可以进一步简化,确保不会重复创建线程
- 捕获异常时尽量避免
except:这种宽泛的捕获,明确捕获特定异常(如Empty、NameError),便于调试
内容的提问来源于stack exchange,提问作者Marcin Łęcki
相关产品推荐
相关产品推荐

