Task Groups

A TaskGroup is the fundamental unit of scheduling and concurrency in Adapnex. Every Task belongs to exactly one task group, which defines its execution model and thread affinity.

Architecture & Thread Affinity

Understanding how task groups map to operating system threads is essential for writing reliable automation systems:

  • Single-Threaded Within a Task Group: All tasks assigned to the same task group run sequentially on a single, dedicated thread. Because they share a thread, tasks within the same task group can share non-atomic variables and mappings without locks or race conditions.

  • Multi-Threaded Across Task Groups: Each task group executes on its own independent thread. Tasks residing in different task groups execute concurrently in parallel. Data shared across different task groups should be synchronized using atomics or thread-safe synchronization primitives.

Adapnex provides two primary types of task groups:

Task Group Type Execution Model Typical Use Case

Cyclic

Fixed periodic interval

Closed-loop control, PID regulation, motion sequencing, fast I/O drivers.

Freewheeling

Continuous execution loop

Background processing, long-running calculations, protocol polling, slow serial devices.

Cyclic Task Groups

A cyclic task group executes at a fixed, periodic interval. It is the primary building block for deterministic, hard real-time industrial control loops.

Create a cyclic task group using Application::CreateCyclicTaskGroup():

// Create a cyclic task group running every 10 milliseconds
const auto control_group = Application::CreateCyclicTaskGroup({
    .period = 10ms,
});

Configuration Parameters

The CyclicTaskGroupConfig struct accepts the following configuration fields:

  • period: The cycle time (such as 1ms, 10ms, or 500us). The runtime dispatches cycles at this exact interval, sleeping on absolute deadlines to minimize jitter.

  • watchdog_time: Cycle execution deadline. Defaults to the cycle period if omitted.

  • watchdog_sensitivity: Number of consecutive overrun cycles tolerated before the watchdog trips.

  • watchdog_handler: Callback invoked if the execution watchdog trips.

Freewheeling Task Groups

A freewheeling task group has no fixed period. Its thread executes the Update() methods of all registered tasks repeatedly in a continuous loop as fast as the CPU allows.

Create a freewheeling task group using Application::CreateFreewheelingTaskGroup():

// Create a freewheeling task group for background processing
const auto worker_group = Application::CreateFreewheelingTaskGroup(0);

Freewheeling task groups are suitable for:

  • Long-running operations that would otherwise overrun a cyclic deadline, such as calculating tool paths or generating trajectory splines.

  • Handling communication protocols with unpredictable response latencies, such as Modbus polling or serial scanners.

  • Writing data records or event logs to storage.

Freewheeling tasks frequently perform blocking operations (such as waiting on network sockets, file I/O, or message queues). When a task blocks, the operating system kernel suspends its thread automatically, freeing the CPU core for other work. If a freewheeling task executes a continuous non-blocking polling loop without I/O waits, it should sleep or yield briefly when idle to avoid needlessly consuming 100% of a CPU core.

Creating Tasks in a Task Group

Tasks should always be created via the task group’s factory method rather than instantiated directly:

// Creates and registers an instance of FastControlTask inside the control task group
const auto task = control_group->CreateTask<FastControlTask>(/* constructor arguments */);

Lambda Tasks

For small logic blocks or quick signal converters, you can create a LambdaTask without defining a new class:

control_group->CreateTask<LambdaTask>([&]() {
    heartbeat_led = !heartbeat_led;
});

Care should be taken to ensure the lifetime of any objects captured by reference in a LambdaTask is sufficiently long to prevent accessing invalid memory. When referencing shared objects, capture std::shared_ptr by value rather than by reference.

Synchronization Across Task Groups

When partitioning an application across multiple task groups (such as a fast cyclic control task group and a background freewheeling processing task group), follow these synchronization guidelines:

  1. Prefer Lock-Free Atomics: When exchanging flags, setpoints, or status words between task groups, use std::atomic<T> with relaxed memory ordering. Lock-free operations guarantee that the real-time cyclic thread is never blocked waiting for a background thread.

  2. Use Mutex When Locking is Necessary: If complex shared state requires mutual exclusion across threads, use Mutex. On real-time platforms, Mutex enables priority inheritance, ensuring that a low-priority background thread holding the mutex does not inadvertently block a high-priority control thread indefinitely.

  3. Double Buffering: For larger structures or recipe blocks, use double buffers with atomic index swapping so the background thread prepares data off-line and the cyclic thread switches buffers in a single atomic step.

The following example demonstrates how a 5ms cyclic control loop coordinates with a background freewheeling task that performs a long-running trajectory calculation:

#include "adapnex.h"
#include <atomic>
#include <chrono>
#include <thread>

// High-speed real-time control task (5ms cycle)
class AxisControlTask final : public Task {
public:
    std::atomic<float> target_position = 0.0f;
    std::atomic<bool> new_trajectory_ready = false;

    void Update() override {
        // Check if the background task published a new target
        if (new_trajectory_ready.load(std::memory_order_relaxed)) {
            current_target = target_position.load(std::memory_order_relaxed);
            new_trajectory_ready.store(false, std::memory_order_relaxed);
        }

        // Execute deterministic motion profile calculation
        // ...
    }

private:
    float current_target = 0.0f;
};

// Background freewheeling task for heavy trajectory optimization
class TrajectoryPlannerTask final : public Task {
public:
    explicit TrajectoryPlannerTask(std::shared_ptr<AxisControlTask> axis)
        : axis_task(std::move(axis)) {}

    void Update() override {
        // Simulate a computationally expensive path-planning algorithm
        std::this_thread::sleep_for(std::chrono::milliseconds(250));

        // Publish the computed trajectory endpoint to the real-time task
        axis_task->target_position.store(125.5f, std::memory_order_relaxed);
        axis_task->new_trajectory_ready.store(true, std::memory_order_relaxed);

        // Sleep until the next trajectory planning request
        std::this_thread::sleep_for(std::chrono::seconds(2));
    }

private:
    std::shared_ptr<AxisControlTask> axis_task;
};

void setup() {
    // 1. Create real-time 5ms cyclic task group for axis control
    const auto control_group = Application::CreateCyclicTaskGroup({.period = 5ms});
    const auto axis = control_group->CreateTask<AxisControlTask>();

    // 2. Create background freewheeling task group for long-running trajectory planning
    const auto planner_group = Application::CreateFreewheelingTaskGroup(0);
    planner_group->CreateTask<TrajectoryPlannerTask>(axis);
}