diff --git a/src/imagemodel.cpp b/src/imagemodel.cpp index 27f2401..2f2f702 100644 --- a/src/imagemodel.cpp +++ b/src/imagemodel.cpp @@ -240,6 +240,9 @@ void ImageModel::queueRow(int row) m_thumbnailSize, &m_cancelFlag); + // THREADING: Explicit QueuedConnection ensures thumbnailReady() is invoked + // on the main thread even though the signal is emitted from a pool thread. + // This means all m_items[] mutations happen on the main thread only. connect(worker, &ThumbnailWorker::finished, this, &ImageModel::thumbnailReady, Qt::QueuedConnection); diff --git a/src/imageviewwidget.cpp b/src/imageviewwidget.cpp index 0ee4d4e..5e83fab 100644 --- a/src/imageviewwidget.cpp +++ b/src/imageviewwidget.cpp @@ -19,6 +19,12 @@ ImageLoadWorker::ImageLoadWorker(const QByteArray &path, QObject *parent) { } +// THREADING: This function runs on its own QThread, NOT the main thread. +// It must not access any ImageViewWidget member state. The only data it uses +// is m_path which is immutable after construction. The emitted imageLoaded() +// signal is delivered to the main thread via Qt::AutoConnection which resolves +// to QueuedConnection at emit time (worker thread != receiver's main-thread +// affinity), so onImageLoaded() always executes on the main thread. void ImageLoadWorker::run() { int fd = ::open(m_path.constData(), O_RDONLY | O_CLOEXEC); @@ -133,6 +139,10 @@ void ImageViewWidget::prefetchImage(const QByteArray &path) m_pendingLoads.insert(path); auto *worker = new ImageLoadWorker(path, this); + // THREADING: Qt::AutoConnection (default) resolves to QueuedConnection at + // emit time because ImageLoadWorker::run() emits from its worker thread + // while 'this' (ImageViewWidget) has main-thread affinity. This guarantees + // onImageLoaded() always runs on the main thread — no mutex needed. connect(worker, &ImageLoadWorker::imageLoaded, this, &ImageViewWidget::onImageLoaded); connect(worker, &QThread::finished, worker, &QObject::deleteLater); worker->start(); diff --git a/src/mainwindow.cpp b/src/mainwindow.cpp index 0fd7ed4..be58306 100644 --- a/src/mainwindow.cpp +++ b/src/mainwindow.cpp @@ -580,6 +580,13 @@ void MainWindow::setFullScreenMode(bool fullScreen) void MainWindow::keyPressEvent(QKeyEvent *event) { + // THREADING: No race condition with F11 fullscreen toggle. All cross-thread + // signal deliveries (thumbnailReady via explicit QueuedConnection, + // onImageLoaded via AutoConnection resolving to QueuedConnection) execute on + // the main thread's event loop. The event loop processes events one at a + // time, so this handler and any signal slot can never interleave. All widget + // state (m_wasMaximized, m_stack, fullscreen flag) is only accessed from the + // main thread. if (event->key() == Qt::Key_F11 && m_stack->currentWidget() == m_imageView) { setFullScreenMode(!isFullScreen()); diff --git a/src/thumbnailcache.cpp b/src/thumbnailcache.cpp index 8f6ad00..f5df06e 100644 --- a/src/thumbnailcache.cpp +++ b/src/thumbnailcache.cpp @@ -59,6 +59,9 @@ bool ThumbnailCache::isInsideCache(const QByteArray &absPath) return absPath.startsWith(root + '/') || absPath == root; } +// THREADING: Called from worker threads (ThumbnailWorker::run()). This +// function is free of shared mutable state — it only performs filesystem I/O +// using its arguments. Safe to call concurrently from multiple threads. QImage ThumbnailCache::load(const QByteArray &sourcePath, Size size) { struct ::stat st{}; @@ -81,6 +84,10 @@ QImage ThumbnailCache::load(const QByteArray &sourcePath, Size size) return img; } +// THREADING: Called from worker threads (ThumbnailWorker::run()). This +// function is free of shared mutable state — it only performs filesystem I/O +// using its arguments. Writes are atomic (temp file + rename) so concurrent +// calls for different source files are safe. void ThumbnailCache::save(const QByteArray &sourcePath, Size size, const QImage &thumbImage) { if (thumbImage.isNull()) diff --git a/src/thumbnailworker.cpp b/src/thumbnailworker.cpp index 3e1024f..2eedea3 100644 --- a/src/thumbnailworker.cpp +++ b/src/thumbnailworker.cpp @@ -12,6 +12,7 @@ namespace { constexpr int kDisplaySize = 128; +// THREADING: Pure function called from a worker thread. No shared mutable state. QImage downscaleForDisplay(const QImage &src) { if (src.isNull()) @@ -36,6 +37,12 @@ ThumbnailWorker::ThumbnailWorker(const QByteArray &path, setAutoDelete(true); } +// THREADING: This function runs on a QThreadPool worker thread, NOT the main +// thread. It must not access any ImageModel state. The only shared data it +// touches is the atomic m_cancelFlag (for cooperative cancellation). All other +// members (m_path, m_row, m_size) are immutable after construction. Results +// are delivered to the main thread via the finished() signal connected with +// Qt::QueuedConnection. void ThumbnailWorker::run() { if (*m_cancelFlag)