git repos / SagoImageBrowser

commit 4d6b7476

Poul Sander · 2026-05-30 16:39
4d6b74767ab112b57f120df8dfee2b405817b5e1 patch · browse files
parent 4db3041aa5124ab367c9f70be0c3a9ed62a38294

Documented all functions that runs in threads

Changed files

M src/imagemodel.cpp before
M src/imageviewwidget.cpp before
M src/mainwindow.cpp before
M src/thumbnailcache.cpp before
M src/thumbnailworker.cpp before
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)