-
-
Notifications
You must be signed in to change notification settings - Fork 276
Using _TASK_THREAD_SAFE Compile Option
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_SAFEcompile option - v4.0.0 (2024-10-26): Major rework for pre-emptive environments with improved architecture
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.
When _TASK_THREAD_SAFE is enabled:
-
Direct calls to Task and Scheduler methods should only be made from the thread where the scheduler's
execute()method runs -
Indirect calls from other threads (including ISRs) should be made through the
Scheduler::requestAction()methods - Requests are placed in a queue and processed by the scheduler during its execution cycle
The system uses a request queue to communicate between threads:
Thread A (ISR/Other) Main Thread (Scheduler)
| |
| requestAction() |
| ─────────────► [Queue] ────► | processRequests()
| | └─► execute tasks
Add the compile flag to your build configuration:
# platformio.ini
[env]
build_flags =
-D _TASK_THREAD_SAFEOr in Arduino IDE, add to your main sketch before including TaskScheduler:
#define _TASK_THREAD_SAFE
#include <TaskScheduler.h>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);#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);
}
}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 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
);Request types are defined in TaskSchedulerDeclarations.h. Common examples include:
-
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_REQUEST_SETINTERVAL- Change task interval -
TASK_REQUEST_SETITERATIONS- Change iterations -
TASK_REQUEST_DELAY- Delay task execution -
TASK_REQUEST_FORCENEXTITERATION- Force immediate execution
-
TASK_SR_REQUEST_SIGNAL- Signal status request -
TASK_SR_REQUEST_SIGNALCOMPLETE- Complete status request -
TASK_SR_REQUEST_SETWAITING- Set waiting state
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);
}void workerThread() {
// Restart task with 5000ms delay
ts.requestAction(&myTask, TASK_REQUEST_RESTARTDELAYED, 5000, 0, 0, 0, 0);
}void adjustTaskSpeed() {
// Set new interval to 2000ms
ts.requestAction(&myTask, TASK_REQUEST_SETINTERVAL, 2000, 0, 0, 0, 0);
}StatusRequest dataReady;
void IRAM_ATTR dataReadyISR() {
// Signal completion from ISR
ts.requestAction(&dataReady, TASK_SR_REQUEST_SIGNALCOMPLETE, 0, 0, 0, 0, 0);
}- Set
TS_QUEUE_LENbased on your application's request frequency - Monitor queue overflow errors during development
- Typical values: 8-32 requests
-
Enqueue wait time: Set based on criticality
- ISRs: Handled automatically (no wait)
- Normal tasks: 5-20ms typical
- Dequeue wait time: Usually 0 (non-blocking)
bool success = ts.requestAction(&myTask, TASK_REQUEST_ENABLE, 0, 0, 0, 0, 0);
if (!success) {
// Queue full or other error
// Handle gracefully
}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)
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()
Monitor for:
- Queue overflow messages
- Failed enqueue operations
- Request processing delays
-
Queue overflow: Increase
TS_QUEUE_LENor reduce request rate -
Slow response: Check
TS_ENQUEUE_WAIT_MSsetting -
Requests ignored: Ensure scheduler
execute()is running regularly - Crashes: Verify all direct task calls are from scheduler thread only
- 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)
- ESP32 (FreeRTOS)
- ESP8266 (RTOS SDK)
- STM32 (FreeRTOS)
- Generic FreeRTOS implementations
- Zephyr RTOS
ESP32/ESP8266:
- Use
IRAM_ATTRfor ISR functions - Queue functions automatically handle ISR context
- Built-in
xPortInIsrContext()detection
STM32:
- Ensure FreeRTOS is properly configured
- May need CMSIS wrapper functions
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
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.