During iteration of Modbus-Tools, we met a classic concurrency issue: the UI window closed, but the process stayed alive and the debug session could not exit cleanly. After addressing that, timeout misjudgment and re-entrancy contention also surfaced. This note records the diagnosis and fixes.
Symptom 1: Hang During Shutdown
- Main thread waits for worker thread termination while destroying
MainWindow - Worker thread may still be blocked on serial or socket I/O
- UI is gone, but process remains
Cause 1: Inconsistent Thread Affinity
QSerialPort and QTcpSocket rely on the event loop of their owning thread. If created in the main thread but effectively used from worker-side flow, shutdown can fall into cross-thread waiting.
Fix 1: Make Ownership Explicit
Move the channel object to the worker thread right after construction:
stack.thread = std::make_shared<QThread>();
stack.channel->moveToThread(stack.thread.get());
Also keep parent unset at creation time so migration remains valid.
Symptom 2: Intermittent Timeout After Connect
- Packet monitor shows device responses
- Business layer still returns timeout
Cause 2: Blocking Wait Starves Event Loop
After sending requests, condition_variable::wait_until blocked the I/O thread. As a result, readyRead callbacks were delayed even when data had already arrived.
Fix 2: Replace With Event-Yielding Wait Loop
while (true) {
QCoreApplication::processEvents(QEventLoop::AllEvents);
if (signalReceived) break;
if (timeout) return Error;
std::this_thread::sleep_for(std::chrono::milliseconds(1));
}
This keeps responsiveness within a controlled loop and avoids busy-spin.
Symptom 3: Self-Deadlock Under High-Frequency Operations
After introducing processEvents, re-entrant calls could happen within the same thread during the waiting window, causing lock contention on the same request path.
Cause 3: Lock Type Mismatch With Re-entrancy
std::mutex does not allow repeated acquisition by the same thread.
Fix 3: Use Recursive Mutex for Request Serialization
std::recursive_mutex requestMutex_;
Takeaways
- Fix I/O object thread ownership early and keep it consistent through lifecycle.
- Avoid long blocking waits in I/O threads; if waiting is required, use an event-aware strategy.
- Whenever event yielding is introduced, re-check re-entrancy paths and lock model together.
This fix not only removed shutdown stalls, but also improved timeout consistency in Modbus communication. For industrial software, deterministic shutdown and explainable timing are as important as functional completeness.