• Switch to Warm
  • Switch to Cool
  • Workspace
  • Projects
  • Notes

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.