I fed 12,000 thumbnail jobs into a ThreadWeaver experiment today. The queue accepted them with admirable composure. The application then spent more time managing tiny jobs than producing thumbnails.

The mechanism is straightforward: jobs enter a queue, worker threads take runnable jobs, and completion is reported back. This is much better than creating one thread per thumbnail. Threads have stacks, scheduling cost, and an unfortunate tendency to outlive the code that launched them.

My mistake was choosing a job that was too small. Each item performed a little path work, checked a cache, and often returned without decoding anything. Queue locking and signal delivery became a noticeable fraction of total work.

Batching nearby items helped:

class ThumbnailBatch : public ThreadWeaver::Job
{
public:
    ThumbnailBatch(const QStringList &paths)
        : m_paths(paths)
    {
    }

protected:
    void run()
    {
        foreach (const QString &path, m_paths)
            createThumbnail(path);
    }

private:
    QStringList m_paths;
};

The batch is large enough to amortise scheduling but small enough that several workers remain busy. There is no universal number. Disk latency, decoding cost, and cache hit rate all change the useful grain size, so I am measuring rather than engraving 32 on a tablet.

Cancellation also becomes a design issue. Removing queued jobs is easy; stopping one already decoding is cooperative. Long operations need checkpoints, and the job must own or safely reference everything it touches. A window closing while a worker holds its raw pointer is not cancellation. It is delayed excitement.

I prefer a shared job queue such as ThreadWeaver for independent background work because it bounds concurrency and centralises scheduling. The caveat is that it does not make unsafe code safe. Shared caches still need locking, GUI objects still belong to the GUI thread, and dependencies between jobs still need explicit representation.

Priority needs restraint as well. Marking every visible thumbnail urgent merely recreates a first-in queue with more bookkeeping. I reserve priority for work whose delay is observable now, and still ensure ordinary jobs eventually run.

The current prototype sends immutable input into a job and returns a completed image through a queued signal. That copy may cost something, but the ownership story fits in one sentence. For now, clarity beats shaving a copy whose cost I have not measured.