You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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 

问题根源

  1. GIL的限制:Python全局解释器锁(GIL)导致同一时刻只有一个线程执行Python字节码。Qt线程池启动的大量QRunnable任务会频繁竞争GIL,挤占UI线程的执行时间,引发卡顿。
  2. 批量任务的瞬间完成:当大量图片加载任务被一次性提交后,线程池会快速调度执行。如果图片体积小或存储速度快,多数任务会在UI线程完成模型更新、触发视图重绘前就已结束,导致data方法直接返回pixmap,占位图无显示机会;同时一次性渲染大量pixmap会造成主线程短暂阻塞。
  3. 模型与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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.19 12:44:55