Task Groups
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 as1ms,10ms, or500us). 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 |
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:
-
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. -
Use
MutexWhen Locking is Necessary: If complex shared state requires mutual exclusion across threads, useMutex. On real-time platforms,Mutexenables priority inheritance, ensuring that a low-priority background thread holding the mutex does not inadvertently block a high-priority control thread indefinitely. -
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);
}