Skip to content

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

  1. WebSocket everywhere — Even local contexts use an embedded WebSocket server. This eliminates the local/remote code path distinction and simplifies the codebase.

  2. 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.

  3. 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.

  4. Atomic state reads — Engine state (Running, Stopped, etc.) is stored as std::atomic so the GUI can poll it without locking.

See Also