PySide6并发读取图片阻塞主线程问题咨询
问题:Qt并发加载大量图片时UI卡顿且占位图无法显示
尽管采用并发方式加载图片,但加载包含大量图片的文件夹时UI仍会卡顿。
实现逻辑
在回调函数中,pixmap loader创建futures并立即添加至模型。模型的data方法在future未完成时返回None,完成时返回pixmap。QListView通过delegate显示pixmaps。
然而,在调用data方法前,所有futures已完成任务,主线程会短暂卡顿,用户始终无法看到占位图。
有趣的是,若在并发future-worker中添加QThread.msleep,用户就能看到占位图(data()返回None),但所有图片的加载时长会大幅增加——大量线程会休眠100ms以上。
相关代码
# callback def on_load_directory(image_filenames): images_futures = [pixmap_loader.load(image_name) for image_name in image_filenames] logger.debug("Futures ready, adding to model") self.pixmap_model.add_pixmaps(images_futures) # model def data(self, index, role): annotated_pixmap_future = self.pixmaps[index.row()] if not annotated_pixmap_future.is_finished(): logger.debug("i never return none") return None annotated_pixmap = annotated_pixmap_future.get() if role == Qt.DisplayRole: return annotated_pixmap # delegate def paint(self, painter, option, index): pixmap = index.data(Qt.DecorationRole) if not pixmap_thumbnail: pixmap = self.placeholder # ... # loading image: def _read(self, filename: str|Path) -> AnnotatedPixmap: pixmap = QPixmap.fromImage(QImage(str(filename))) thumbnail = pixmap.scaled(self.thumbnail_size, self.thumbnail_size, aspectMode=Qt.KeepAspectRatio) return AnnotatedPixmap(pixmap, thumbnail, str(filename)) def load(self, filename: str|Path) -> SimpleFutureWorker[AnnotatedPixmap]: worker = SimpleFutureWorker(self._read, filename=filename) worker.signals.result.connect(self._on_load_finished) self.load_cache[(str(filename))] = True self.thread_pool.start(worker) return worker # future # simple - maybe need some refactoring, but it works class SimpleFutureWorker(Generic[T], QRunnable): _SEMAPHORE_GUADS = 1000 def __init__(self, fn: Callable[..., T], *args, **kwargs): super(SimpleFutureWorker, self).__init__() # Store constructor arguments (re-used for processing) self.fn = fn self.args = args self.kwargs = kwargs self.signals = WorkerSignals() self.result = None self._is_running = False self._is_finished = False # maybe could be done better with latch but I don't know if qt has one self._semaphore = QSemaphore(SimpleFutureWorker._SEMAPHORE_GUADS) self._semaphore.acquire(SimpleFutureWorker._SEMAPHORE_GUADS) # Add the callback to our kwargs # self.kwargs['progress_callback'] = self.signals.progress def run(self) -> None: ''' Initialise the runner function with passed args, kwargs. ''' # Retrieve args/kwargs here; and fire processing using them self._is_running = True try: # Sleeping thread - optional. # from PySide6.QtCore import QThread # QThread.msleep(100) self.result = self.fn(*self.args, **self.kwargs) except: traceback.print_exc() exctype, value = sys.exc_info()[:2] self.signals.error.emit((exctype, value, traceback.format_exc())) else: self.signals.result.emit(self.result) # Return the result of the processing finally: self._is_finished = True self._is_running = True self.signals.finished.emit() # Done self._semaphore.release(SimpleFutureWorker._SEMAPHORE_GUADS) def is_finished(self) -> bool: return self._is_finished def is_running(self) -> bool: return self._is_running def get(self) -> T: self._semaphore.acquire(1) result = self.result self._semaphore.release(1) return result
问题根源
- GIL的限制:Python全局解释器锁(GIL)导致同一时刻只有一个线程执行Python字节码。Qt线程池启动的大量
QRunnable任务会频繁竞争GIL,挤占UI线程的执行时间,引发卡顿。 - 批量任务的瞬间完成:当大量图片加载任务被一次性提交后,线程池会快速调度执行。如果图片体积小或存储速度快,多数任务会在UI线程完成模型更新、触发视图重绘前就已结束,导致
data方法直接返回pixmap,占位图无显示机会;同时一次性渲染大量pixmap会造成主线程短暂阻塞。 - 模型与future的交互逻辑缺陷:模型在
data方法中主动判断future状态的设计,无法保证UI线程先渲染占位图再加载图片。一旦future提前完成,占位图逻辑直接被跳过。
可行解决方案
1. 限制线程池并发数
Python受GIL限制,过高的并发数会增加线程切换开销。手动设置合理的并发数,平衡加载速度与UI响应:
self.thread_pool.setMaxThreadCount(4) # 根据CPU核心数调整,一般2-4即可
2. 分批提交加载任务
不要一次性提交所有任务,分批次提交并在每批提交后让UI线程处理事件,确保占位图先显示:
def on_load_directory(image_filenames): batch_size = 20 # 每批处理20张图片 for i in range(0, len(image_filenames), batch_size): batch = image_filenames[i:i+batch_size] images_futures = [pixmap_loader.load(image_name) for image_name in batch] self.pixmap_model.add_pixmaps(images_futures) QApplication.processEvents() # 让UI线程处理重绘等事件
3. 改用信号驱动的模型更新
让模型初始时存储占位状态,当图片加载完成后通过信号通知主线程更新模型,触发视图重绘:
# 修改pixmap_loader的完成回调 def _on_load_finished(self, result): # 根据filename找到模型中对应的索引 index = self.pixmap_model.get_index_by_filename(result.filename) if index.isValid(): self.pixmap_model.update_pixmap(index, result) # 修改模型逻辑 def __init__(self): self.pixmap_data = [] # 存储None(占位)或已加载的AnnotatedPixmap super().__init__() def add_pixmaps(self, filenames): # 初始添加占位状态 for filename in filenames: self.pixmap_data.append(None) self.layoutChanged.emit() def update_pixmap(self, index, annotated_pixmap): self.pixmap_data[index.row()] = annotated_pixmap self.dataChanged.emit(index, index) def data(self, index, role): if role == Qt.DisplayRole: data = self.pixmap_data[index.row()] return data.thumbnail if data else None
4. 优化图片加载逻辑
将图片缩放等操作尽量放在C++层面执行,减少Python代码的执行时间,降低GIL竞争:
def _read(self, filename: str|Path) -> AnnotatedPixmap: image = QImage(str(filename)) # 直接用QImage缩放,避免先转QPixmap再缩放 thumbnail_image = image.scaled(self.thumbnail_size, self.thumbnail_size, aspectMode=Qt.KeepAspectRatio) return AnnotatedPixmap( QPixmap.fromImage(image), QPixmap.fromImage(thumbnail_image), str(filename) )
5. 移除SimpleFutureWorker的阻塞逻辑
避免在data方法中调用get(),完全通过信号驱动更新,防止主线程意外阻塞。
内容的提问来源于stack exchange,提问作者Michal Kurtys
相关产品推荐
相关产品推荐

