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::array when dimensions are known at compile time.

  • Reserve capacity in dynamic containers such as std::vector during initialization rather than letting them grow during cycle execution.

  • Avoid heap-allocated temporary strings such as dynamic std::string concatenation inside Update(). Instead, use fixed-capacity strings such as boost::static_string on cyclic threads. Boost ships bundled with the Adapnex SDK.

Examples

Avoid: Dynamic memory allocation during cyclic execution
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);
}
Recommended: Pre-allocating storage and using fixed-capacity strings
#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::cout or printf invokes 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, use TCPConnection or UDPConnection, which provide non-blocking transfer options to avoid stalling cyclic threads. Alternatively, handle socket communication in a separate freewheeling task group.

Examples

Avoid: Blocking operations on cyclic threads
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);
}
Recommended: Non-blocking cyclic execution
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, Mutex guarantees 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.

  • Mutex conforms to C++ BasicLockable and Lockable requirements, allowing direct use with standard RAII wrappers like std::lock_guard and std::unique_lock.

Examples

Avoid: Using std::mutex across real-time and background threads
// Standard mutex does not guarantee priority inheritance on real-time targets
std::mutex standard_lock;
Recommended: Using Mutex for priority inheritance
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.