PiPLC Threading Model¶
PiPLC uses a multi-threaded architecture with an embedded WebSocket server. Thread safety is achieved through a combination of locks, atomics, and Qt's signal/slot queuing.
Thread Layout¶
Main Thread (Qt GUI) Embedded Server Thread
───────────────────── ────────────────────────────────
MainWindow EngineServer + QWebSocketServer
LadderEditor ClientSession per connected client
SimulationPanel Heartbeat timer
WatchPanel
SignalRouterPanel Engine Worker Threads (per context)
RemoteConnection(s) ────────────────────────────────
├─ localhost:auto (embedded) ExecutionEngine 1 ("Default")
└─ 192.168.x.x:9100 (remote) MemoryManager 1 (QReadWriteLock)
LadderExecutor 1, QTimer (scan)
ExecutionEngine 2 ("Context 2")
MemoryManager 2 (QReadWriteLock)
LadderExecutor 2, QTimer (scan)
SignalRouter Worker Thread
────────────────────────────────
QTimer (10ms base tick)
reads MemMgr A → writes MemMgr B
Thread Responsibilities¶
Main Thread (Qt GUI)¶
Runs the Qt event loop and all UI widgets. Holds RemoteEngineContext proxies that communicate with engine servers via WebSocket. Memory reads from the UI come from locally cached snapshots — no cross-thread locking needed on the GUI thread.
Embedded Server Thread¶
Runs a QWebSocketServer on localhost:0 (OS-assigned port). Manages ClientSession objects for each connected editor. Also used by standalone piplc-engine instances on remote hardware.
Engine Worker Threads (one per context)¶
Each EngineContext runs its ExecutionEngine on a dedicated QThread. The scan cycle runs on a QTimer and acquires a write lock on the MemoryManager during executeScan().
SignalRouter Worker Thread¶
Runs server-side on a dedicated thread. A QTimer fires at a 10ms base tick. The router reads from source context memory managers and writes to destination context memory managers, using read/write locks respectively.
Thread Safety Mechanisms¶
| Mechanism | Location | Purpose |
|---|---|---|
QReadWriteLock | MemoryManager | Scan cycle holds write lock; SignalRouter and snapshot sender hold read locks. Multiple readers can run concurrently. |
QMutex | MemoryManager::m_forceLock | Protects the forced-bits hash (m_forcedBits) for thread-safe forcing from the UI while the executor reads forces during scan. |
std::atomic<ExecutionState> | ExecutionEngine | Lock-free cross-thread reads of engine state. |
QMutex | ScanStatistics | Protects statistics updates and reads across threads. |
QMutex | SignalRouter::m_routes | Protects the route table. Route syncs arrive via ClientSession; the router thread takes a snapshot under the lock then iterates without holding it. |
std::atomic<bool> | SignalRouter::m_enabled | Lock-free check for router enable/disable. |
QMetaObject::invokeMethod | EmbeddedEngineServer | BlockingQueuedConnection for cross-thread context management (create/remove contexts). |
QMetaObject::invokeMethod | RemoteConnection::sendCommand() | Marshals cross-thread WebSocket calls to the server thread's event loop. |
| Signal/Slot auto-queuing | Qt framework | Qt automatically uses queued connections for signals crossing thread boundaries. |
Data Flow¶
┌──────────────┐
│ GUI Thread │
│ │
│ RemoteEngine │──── WebSocket ────┐
│ Context │ │
│ (cached │ ▼
│ snapshots) │ ┌─────────────────┐
└──────────────┘ │ Server Thread │
│ │
│ ClientSession │
│ ├─ JSON cmds │
│ └─ binary snap │
└────────┬────────┘
│
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Worker Thd 1 │ │ Worker Thd 2 │ │ Router Thd │
│ Engine "Def" │ │ Engine "PLC2"│ │ SignalRouter │
│ MemMgr (WrLk)│ │ MemMgr (WrLk)│ │ RdLk → WrLk │
└──────────────┘ └──────────────┘ └──────────────┘
Key Design Decisions¶
-
WebSocket everywhere — Even local contexts use an embedded WebSocket server. This eliminates the local/remote code path distinction and simplifies the codebase.
-
Read-write lock on memory — Allows the snapshot sender (read) and SignalRouter (read source) to run concurrently with each other, only blocking when the scan cycle (write) is active.
-
Snapshot-then-iterate for routes — The router copies the route table under
QMutex, then releases the lock before iterating. This prevents route table updates from blocking the router's execution loop. -
Atomic state reads — Engine state (
Running,Stopped, etc.) is stored asstd::atomicso the GUI can poll it without locking.
See Also¶
- Architecture Overview — System architecture and component descriptions
- Protocol Specification — WebSocket message format