Const Does Not Mean Concurrent
A cache lookup crashed under load today even though every worker called a const method. That sentence sounds reassuring only if const is mistaken for a locking primitive.
The method updated a mutable cache:
QImage ImageStore::image(const QString &key) const
{
if (!m_cache.contains(key))
m_cache.insert(key, loadImage(key));
return m_cache.value(key);
}
const prevents ordinary modification through the public type. It says nothing about simultaneous calls, and mutable explicitly allows internal state to change. Two processors can both inspect and modify the cache, corrupting its internal bookkeeping.
I first added a mutex around the whole operation. That was correct but serialised disk loading, so unrelated images waited behind one slow file. The improved version checks under lock, loads outside it, then locks again to publish:
QImage ImageStore::image(const QString &key) const
{
m_mutex.lock();
if (m_cache.contains(key)) {
QImage image = m_cache.value(key);
m_mutex.unlock();
return image;
}
m_mutex.unlock();
QImage image = loadImage(key);
m_mutex.lock();
if (!m_cache.contains(key))
m_cache.insert(key, image);
else
image = m_cache.value(key);
m_mutex.unlock();
return image;
}
Duplicate loading is possible, but publication is safe and the common cached path is short. For expensive loads I might track in-progress keys with a wait condition; that complexity needs measurements first.
Implicitly shared Qt values make returning an image cheap in the usual case, but they do not make shared storage safe for concurrent mutation. Copy-on-write protects value semantics, not arbitrary access patterns.
The lock itself belongs beside the state it protects. Passing the cache to helper code without the mutex invites a second access path that quietly ignores the rule. I am documenting the invariant as “all cache lookup and publication occurs while holding m_mutex,” then checking each method against that sentence.
I prefer immutable inputs and per-job results when using several cores. Shared caches should be small, explicitly locked, and treated as an optimisation rather than the foundation of correctness. The caveat is memory and copying: some data is too large to duplicate, so careful shared ownership remains necessary.
The failure appeared only on the dual-core machine and only after several minutes. That does not make it a rare bug. It makes the single-core test machine an excellent accomplice.