Skip to content

Using _TASK_THREAD_SAFE Compile Option

Anatoli Arkhipenko edited this page Oct 7, 2025 · 2 revisions

TaskScheduler _TASK_THREAD_SAFE Compile Option

Overview

The _TASK_THREAD_SAFE compile option enables thread-safe operation of the TaskScheduler library for multi-core systems or when running under RTOS (Real-Time Operating Systems) like FreeRTOS or Zephyr.

Version History:

  • v3.6.0 (2021-11-01): Initial introduction of _TASK_THREAD_SAFE compile option
  • v4.0.0 (2024-10-26): Major rework for pre-emptive environments with improved architecture

Purpose

In multi-threaded or multi-core environments, directly calling Task and Scheduler methods from different threads can cause race conditions and data corruption. The _TASK_THREAD_SAFE option provides a mechanism to safely interact with tasks from threads other than the one running the scheduler's execute() method.

Key Concepts

Thread Safety Model

When _TASK_THREAD_SAFE is enabled:

  1. Direct calls to Task and Scheduler methods should only be made from the thread where the scheduler's execute() method runs
  2. Indirect calls from other threads (including ISRs) should be made through the Scheduler::requestAction() methods
  3. Requests are placed in a queue and processed by the scheduler during its execution cycle

Request Queue Architecture

The system uses a request queue to communicate between threads:

Thread A (ISR/Other)          Main Thread (Scheduler)
     |                               |
     | requestAction()              |
     | ─────────────► [Queue] ────► | processRequests()
     |                               | └─► execute tasks

Enabling the Feature

Add the compile flag to your build configuration:

# platformio.ini
[env]
build_flags =
    -D _TASK_THREAD_SAFE

Or in Arduino IDE, add to your main sketch before including TaskScheduler:

#define _TASK_THREAD_SAFE
#include <TaskScheduler.h>

Implementation Requirements

1. Implement Queue Functions

You must implement two functions that handle the request queue:

bool _task_enqueue_request(_task_request_t* req);
bool _task_dequeue_request(_task_request_t* req);

FreeRTOS Example Implementation

#include <freertos/FreeRTOS.h>
#include <freertos/queue.h>

// Define queue parameters
#define TS_QUEUE_LEN 16
#define TS_ENQUEUE_WAIT_MS 10  // Max wait time for enqueue
#define TS_DEQUEUE_WAIT_MS 0   // No wait for dequeue

// Queue variables
QueueHandle_t tsQueue;
uint8_t tsQueueData[TS_QUEUE_LEN * sizeof(_task_request_t)];
StaticQueue_t tsQueueBuffer;

// Initialize queue (call during setup)
void initTaskSchedulerQueue() {
  tsQueue = xQueueCreateStatic(
    TS_QUEUE_LEN,
    sizeof(_task_request_t),
    tsQueueData,
    &tsQueueBuffer
  );
}

// Enqueue request (handles both ISR and normal context)
bool _task_enqueue_request(_task_request_t* req) {
  if (xPortInIsrContext()) {
    // Called from ISR
    BaseType_t xHigherPriorityTaskWokenByPost = pdFALSE;
    BaseType_t rc = xQueueSendFromISR(tsQueue, req, &xHigherPriorityTaskWokenByPost);
    if (xHigherPriorityTaskWokenByPost) portYIELD_FROM_ISR();
    return rc;
  }
  else {
    // Called from normal task context
    return xQueueSend(tsQueue, req, TS_ENQUEUE_WAIT_MS);
  }
}

// Dequeue request (handles both ISR and normal context)
bool _task_dequeue_request(_task_request_t* req) {
  if (xPortInIsrContext()) {
    // Called from ISR
    BaseType_t xHigherPriorityTaskWokenByPost = pdFALSE;
    BaseType_t rc = xQueueReceiveFromISR(tsQueue, req, &xHigherPriorityTaskWokenByPost);
    if (xHigherPriorityTaskWokenByPost) portYIELD_FROM_ISR();
    return rc;
  }
  else {
    // Called from normal task context
    return xQueueReceive(tsQueue, req, TS_DEQUEUE_WAIT_MS);
  }
}

2. Request Processing

The scheduler automatically processes queued requests during the execute() method:

  • Requests are processed at the start of each scheduler pass
  • Requests are processed between each task execution in the chain
  • All requests are processed in the order they were received (FIFO)

Using requestAction()

Method Signatures

// Using pre-built request structure
bool Scheduler::requestAction(_task_request_t* aRequest);

// Simplified interface
bool Scheduler::requestAction(
    void* aObject,                      // Task or StatusRequest pointer
    _task_request_type_t aType,         // Request type
    unsigned long aParam1,              // Parameter 1
    unsigned long aParam2,              // Parameter 2
    unsigned long aParam3,              // Parameter 3
    unsigned long aParam4,              // Parameter 4
    unsigned long aParam5               // Parameter 5
);

Available Request Types

Request types are defined in TaskSchedulerDeclarations.h. Common examples include:

Task Control Requests

  • TASK_REQUEST_ENABLE - Enable a task
  • TASK_REQUEST_DISABLE - Disable a task
  • TASK_REQUEST_RESTART - Restart a task
  • TASK_REQUEST_ENABLEDELAYED - Enable with delay
  • TASK_REQUEST_ABORT - Abort task execution
  • TASK_REQUEST_CANCEL - Cancel task

Task Parameter Requests

  • TASK_REQUEST_SETINTERVAL - Change task interval
  • TASK_REQUEST_SETITERATIONS - Change iterations
  • TASK_REQUEST_DELAY - Delay task execution
  • TASK_REQUEST_FORCENEXTITERATION - Force immediate execution

StatusRequest Requests

  • TASK_SR_REQUEST_SIGNAL - Signal status request
  • TASK_SR_REQUEST_SIGNALCOMPLETE - Complete status request
  • TASK_SR_REQUEST_SETWAITING - Set waiting state

Usage Examples

Example 1: Enable Task from ISR

Task myTask(1000, TASK_FOREVER, &myCallback);

void IRAM_ATTR buttonISR() {
  // Request task enable from ISR
  ts.requestAction(&myTask, TASK_REQUEST_ENABLE, 0, 0, 0, 0, 0);
}

Example 2: Restart Task with Delay from Another Thread

void workerThread() {
  // Restart task with 5000ms delay
  ts.requestAction(&myTask, TASK_REQUEST_RESTARTDELAYED, 5000, 0, 0, 0, 0);
}

Example 3: Change Task Interval

void adjustTaskSpeed() {
  // Set new interval to 2000ms
  ts.requestAction(&myTask, TASK_REQUEST_SETINTERVAL, 2000, 0, 0, 0, 0);
}

Example 4: Signal StatusRequest from ISR

StatusRequest dataReady;

void IRAM_ATTR dataReadyISR() {
  // Signal completion from ISR
  ts.requestAction(&dataReady, TASK_SR_REQUEST_SIGNALCOMPLETE, 0, 0, 0, 0, 0);
}

Best Practices

1. Queue Size Tuning

  • Set TS_QUEUE_LEN based on your application's request frequency
  • Monitor queue overflow errors during development
  • Typical values: 8-32 requests

2. Wait Times

  • Enqueue wait time: Set based on criticality
    • ISRs: Handled automatically (no wait)
    • Normal tasks: 5-20ms typical
  • Dequeue wait time: Usually 0 (non-blocking)

3. Error Handling

bool success = ts.requestAction(&myTask, TASK_REQUEST_ENABLE, 0, 0, 0, 0, 0);
if (!success) {
  // Queue full or other error
  // Handle gracefully
}

4. Memory Considerations

Request queue size calculation:

Queue Memory = TS_QUEUE_LEN × sizeof(_task_request_t)
             = TS_QUEUE_LEN × ~40 bytes
             = 640 bytes (for TS_QUEUE_LEN=16)

5. Thread Safety Guidelines

SAFE - From any thread/ISR:

  • scheduler.requestAction()
  • Custom queue implementation functions

UNSAFE - Only from scheduler thread:

  • task.enable()
  • task.disable()
  • task.setInterval()
  • Direct task method calls
  • scheduler.execute()

Debugging

Check Queue Status

Monitor for:

  • Queue overflow messages
  • Failed enqueue operations
  • Request processing delays

Common Issues

  1. Queue overflow: Increase TS_QUEUE_LEN or reduce request rate
  2. Slow response: Check TS_ENQUEUE_WAIT_MS setting
  3. Requests ignored: Ensure scheduler execute() is running regularly
  4. Crashes: Verify all direct task calls are from scheduler thread only

Performance Considerations

  • Request processing adds minimal overhead (~microseconds per request)
  • Queue access is lock-free in most RTOS implementations
  • ISR-safe operations use optimized queue functions
  • Bulk request processing is efficient (all processed in one pass)

Platform Support

Tested Platforms

  • ESP32 (FreeRTOS)
  • ESP8266 (RTOS SDK)
  • STM32 (FreeRTOS)
  • Generic FreeRTOS implementations
  • Zephyr RTOS

Platform-Specific Notes

ESP32/ESP8266:

  • Use IRAM_ATTR for ISR functions
  • Queue functions automatically handle ISR context
  • Built-in xPortInIsrContext() detection

STM32:

  • Ensure FreeRTOS is properly configured
  • May need CMSIS wrapper functions

Reference

Related Compile Options

Often used together:

  • _TASK_STATUS_REQUEST - Status request support
  • _TASK_ISR_SUPPORT - ISR-safe methods (ESP only)
  • _TASK_TIMEOUT - Task timeout support
  • _TASK_TICKLESS - FreeRTOS tickless idle support

Conclusion

The _TASK_THREAD_SAFE compile option provides robust thread-safe operation for TaskScheduler in multi-threaded environments. By using the request queue mechanism, you can safely control tasks from any thread or ISR while maintaining data integrity and preventing race conditions.

For the most up-to-date information, refer to the TaskScheduler library documentation and changelog in TaskScheduler.h.

Clone this wiki locally