-
-
Notifications
You must be signed in to change notification settings - Fork 275
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_SAFE
compile 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_SAFE
Or 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_LEN
based 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_LEN
or reduce request rate -
Slow response: Check
TS_ENQUEUE_WAIT_MS
setting -
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_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
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
.