Qt Cross-Thread Signal Delivery — Why Direct Connections Don't Do What You Think on Embedded Linux
Deep dive
Published Mar 18, 2026 · Updated Apr 2, 2026
The assumption
A Qt::DirectConnection delivers the slot synchronously in the sender’s
thread. For a GUI application on a resource-constrained embedded device, this
sounds like exactly what you want — no event loop overhead, no queued
marshalling, just a function call.
The assumption breaks the moment the slot touches anything that Qt’s main thread owns.
Where it breaks
QWidget updates, QOpenGLContext operations, and most QTimer calls are
not thread-safe. A DirectConnection from a worker thread into a slot that
calls QWidget::update() will execute that call on the worker thread, not
the main thread — Qt doesn’t relocate it, it just calls the function. On
x86 with strong memory ordering, this often “works” silently, producing
occasional visual glitches or rare crashes that don’t reproduce under
debugger. On ARM with relaxed ordering, it fails more visibly: corrupted
render buffers, segfaults in the Qt paint engine, or hangs in
QOpenGLFramebufferObject::bind() that never time out.
The subtlety is that Qt::DirectConnection doesn’t mean “call this
where it’s safe.” It means “call this where the signal was emitted.”
The fix that actually holds
For any slot that touches GUI or OpenGL state, use Qt::QueuedConnection
explicitly — or rely on the default when sender and receiver live in
different threads with their own event loops. The queued path serializes
the call through the receiver’s event loop, which means the slot executes
on the receiver’s thread, which is where the GUI state lives.
The cost is real: one heap allocation per queued call (the QMetaCallEvent),
and the delivery latency of one event loop iteration. On a 60fps render
loop that’s up to ~16ms of added latency. For frame-level synchronization
in a medical imaging viewer, that matters — but the alternative (undefined
behavior masked as intermittent crashes) matters more.
The pattern that emerged
After hitting this repeatedly in the realtime imaging system, the rule
became: never use DirectConnection across thread boundaries in a Qt
application that touches OpenGL, even if the slot looks harmless. If the
slot’s body might ever grow to touch GUI state, it’s cheaper to start with
QueuedConnection and optimize later than to debug a crash that only
manifests on ARM at 2am during a demo.
The one exception: signals emitted from the main thread to slots on the
main thread. There, DirectConnection is fine and the event loop overhead
is pointless. The rule is specifically about cross-thread delivery.