Guidelines for Real-Time Programming
In industrial automation and motion control, real-time software aims for determinism. The runtime executes control algorithms and updates I/O within repeatable cycle deadlines. Latency predictability and jitter minimization take precedence over peak throughput.
Adapnex manages real-time scheduling and thread affinity across supported industrial hardware. The guidelines below outline recommended patterns for achieving consistent, low-jitter execution on the cyclic path.
Memory Management
Dynamic heap allocations (malloc, new, free, delete) and dynamic smart pointer instantiations
(std::make_shared, std::make_unique) introduce variable latency because the system allocator may search free lists,
reorganize memory arenas, or request pages from the operating system kernel.
Recommendations
-
Pre-allocate required memory, smart pointers, buffers, and data structures during
Setup(). -
Use fixed-capacity containers such as
std::arraywhen dimensions are known at compile time. -
Reserve capacity in dynamic containers such as
std::vectorduring initialization rather than letting them grow during cycle execution. -
Avoid heap-allocated temporary strings such as dynamic
std::stringconcatenation insideUpdate(). Instead, use fixed-capacity strings such asboost::static_stringon cyclic threads. Boost ships bundled with the Adapnex SDK.
Examples
void Update() override {
// String concatenation allocates memory on the heap
std::string status = "Temperature: " + std::to_string(current_temp);
// Growing a vector without reserving capacity may trigger reallocations
readings.push_back(current_temp);
}
#include "boost/static_string.hpp"
class MonitorTask final : public Task {
public:
void Setup() override {
// Reserve buffer capacity before cyclic execution begins
readings.reserve(1000);
}
void Update() override {
// Safe: fixed-capacity string allocated on the stack without heap allocation
boost::static_string<64> status = "State: RUNNING";
// Safe: appends to pre-allocated storage without triggering reallocation
if (readings.size() < 1000) {
readings.push_back(current_temp);
}
}
private:
float current_temp = 0.0f;
std::vector<float> readings;
};
Non-Blocking Execution
A real-time cyclic thread should avoid blocking on resources with unpredictable response times.
Recommendations
-
Disk I/O: Synchronous file logging or writing to persistent storage can stall a thread for several milliseconds. Buffer log entries in memory or delegate logging to a freewheeling background task group.
-
Console Streaming: Writing to
std::coutorprintfinvokes system calls that can block when operating system terminal buffers fill. -
Network Communication: Blocking socket calls (
read,write,recv,send) wait for remote network acknowledgments. When network operations cannot be moved to a background task group, useTCPConnectionorUDPConnection, which provide non-blocking transfer options to avoid stalling cyclic threads. Alternatively, handle socket communication in a separate freewheeling task group.
Examples
void Update() override {
// Blocking file write stalls the real-time cycle
log_file << "Axis position: " << position << std::endl;
// Blocking network read waits on remote peer
socket.Receive(buffer);
}
void Update() override {
// Perform deterministic calculation without blocking
motor_command = pid(setpoint, feedback);
}
Synchronization & Priority Inheritance
Lock-free programming using atomic operations is the preferred approach for sharing data across tasks in different task
groups. Operations like std::atomic<T>::load() and std::atomic<T>::store() execute in a few processor cycles
without suspending threads.
When synchronizing multi-variable state across threads, mutual exclusion may be necessary. Using standard library locks
(such as std::mutex) can lead to priority inversion, where a low-priority background thread holding a lock is
preempted by an unrelated medium-priority thread, starving the high-priority real-time thread that is waiting for the
lock.
Adapnex provides Mutex, an abstraction designed for real-time applications:
-
On real-time capable platforms,
Mutexguarantees priority inheritance. When a high-priority cyclic thread waits on a mutex held by a lower-priority background thread, the operating system temporarily raises the background thread’s priority to match the waiting thread until the mutex is released. -
Mutexconforms to C++BasicLockableandLockablerequirements, allowing direct use with standard RAII wrappers likestd::lock_guardandstd::unique_lock.
Examples
// Standard mutex does not guarantee priority inheritance on real-time targets
std::mutex standard_lock;
class SharedConfig {
public:
SharedConfig() : mutex(Mutex::Create()) {}
void Write(float gain, float limit) {
std::lock_guard<Mutex> guard(*mutex);
k_gain = gain;
k_limit = limit;
}
void Read(float &gain, float &limit) {
std::lock_guard<Mutex> guard(*mutex);
gain = k_gain;
limit = k_limit;
}
private:
std::unique_ptr<Mutex> mutex;
float k_gain = 1.0f;
float k_limit = 100.0f;
};
Task Partitioning
Separating time-critical logic from background processing helps maintain real-time performance:
-
Fast Cyclic Task Group: Dedicated to tasks with strict deadlines (e.g. 1ms to 10ms cycles), such as closed-loop motion control, safety interlocks, and high-speed I/O.
-
Freewheeling Task Group: Dedicated to tasks without strict deadlines, such as parsing configuration files, recipe loading, database logging, and slow communication protocols.
Data flows between the task groups using relaxed atomics for single values or Mutex for coordinated
multi-variable updates.