Author Topic: Choosing Between Mutex, Semaphore, and Queue in RTOS  (Read 3160 times)

0 Members and 1 Guest are viewing this topic.

Offline EVblog1Topic starter

  • Regular Contributor
  • *
  • Posts: 145
  • Country: 00
Choosing Between Mutex, Semaphore, and Queue in RTOS
« on: July 10, 2026, 01:24:39 pm »
Hello all,

I am trying to understand which one (mutex, semaphore, or queue) is the best choice for any scenario. As I see it, a mutex is best suited to protect a shared resource, a semaphore is best suited for waking up a task when an event occurs, and a queue is used to share data.

For example, suppose a temperature and humidity sensor sends data to the MCU over UART. We have two tasks: a Sensor Task that receives the sensor data and a Terminal Task that sends the received data to a serial terminal over another UART.

My first thought is to use a queue because the Sensor Task needs to pass the temperature and humidity data to the Terminal Task.

I also thought about using a mutex. My concern is that if both tasks access the same global structure, the Sensor Task might update the temperature value and then get preempted before updating the humidity value. If the Terminal Task runs at that moment, it could read the new temperature but the old humidity value, resulting in inconsistent data. I think a mutex would solve this problem by allowing only one task to access the shared structure at a time, ensuring that the Terminal Task always reads a complete and consistent set of data.

So, for this scenario, what would be your preferred approach? Would you use only a queue, only a mutex, or a combination of both? My initial thought is to use a queue along with a mutex, because I feel that using either a queue or a mutex alone may not solve the entire problem.

Do you agree with my approach? If not, could you give me the reason why not?
 

Offline jc101

  • Frequent Contributor
  • **
  • Posts: 871
  • Country: gb
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #1 on: July 10, 2026, 05:10:14 pm »
If the sensor is only read in the sensor task, then I'd just have a queue. The sensor task reads the data as and when it needs to, puts it into the queue, and the terminal task dequeues the data and sends it.

There is no need for any global data structures for the data. It sits in the queue which acts as a FIFO buffer for the data.

I often have multiple sensors all throwing different data types into a common queue, using a union of the various types, and one task to remove the data from the queue and process it as needed. If the terminal task uses portMAXDelay to wait for something in the queue, it will be blocked until it arrives.

The FreeRTOS docs are very good at explaining such things: https://freertos.org/Documentation/02-Kernel/02-Kernel-features/02-Queues-mutexes-and-semaphores/01-Queues
 
The following users thanked this post: EVblog1

Offline peter-h

  • Super Contributor
  • ***
  • Posts: 5993
  • Country: gb
  • Doing electronics since the 1960s...
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #2 on: July 10, 2026, 06:10:58 pm »
This looks like a CS class assignment.

I suggest googling.

I've only ever used a mutex.
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Offline SiliconWizard

  • Super Contributor
  • ***
  • Posts: 17783
  • Country: fr
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #3 on: July 10, 2026, 07:47:58 pm »
So, for this scenario, what would be your preferred approach? Would you use only a queue, only a mutex, or a combination of both? My initial thought is to use a queue along with a mutex, because I feel that using either a queue or a mutex alone may not solve the entire problem.

Do you agree with my approach? If not, could you give me the reason why not?

Depends on how the data is supposed to flow.
A queue is the simplest (for the user) construct that will do it - one task fills the queue with sensor data, another task waits for/reads from the queue, and everybody is happy.
The queue does the synchronization, is thread-safe and allows buffering data.

If you don't need to buffer more than one entry of data at any given time, you could use a mutex instead but a mutex will not tell you if there is new data or not. So you'd use a semaphore to signal that new data is available to read.
If you don't care about knowing if the sensor data is new or not, a simple mutex with nothing else, at the point where you need to read sensor data, is fine. You'll read data the freshness of which is unknown, but it may fit your requirements.

Otherwise you're just going to hand-implement what already exists.
If you manually implement a circular buffer using mutexes and semaphores, then you're wasting your time, as a queue is pretty much exactly that, but battle-proven and won't require debugging your own implementation.
 
The following users thanked this post: EVblog1

Offline EVblog1Topic starter

  • Regular Contributor
  • *
  • Posts: 145
  • Country: 00
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #4 on: July 10, 2026, 11:09:34 pm »
This looks like a CS class assignment.

I suggest googling.

I've only ever used a mutex.
this isn't a homework or assignment question.

I'm an test engineer and an hobbyist. I'm currently learning RTOS concepts and trying to understand the reasoning behind choosing the right synchronization primitive in real-world applications, rather than just memorizing their definitions.  I came up with this scenario myself to test my understanding.
 

Offline EVblog1Topic starter

  • Regular Contributor
  • *
  • Posts: 145
  • Country: 00
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #5 on: July 10, 2026, 11:22:43 pm »
There is no need for any global data structures for the data. It sits in the queue which acts as a FIFO buffer for the data.
You're saying that using a queue alone is sufficient

My understanding is that both tasks need access to the temperature and humidity data. I was assuming there would be a shared global structure where the Sensor Task writes the temperature and humidity values, and the Terminal Task reads them.

If that's the case, couldn't the Sensor Task update the temperature, get preempted before updating the humidity, and then the Terminal Task read the structure? That would result in inconsistent data.

Can you explain reason in detail why a mutex wouldn't be needed in this scenario?
 

Online ledtester

  • Super Contributor
  • ***
  • Posts: 4130
  • Country: us
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #6 on: July 11, 2026, 12:56:47 am »
Why do you even need two tasks?

What's wrong with just using blocking reads and writes in a loop, e.g.:

Code: [Select]
while (1) {
    temp_and_humidity = read_from_uart_1();
    write_to_uart_2(temp_and_humidity);
}

I'm not saying you don't need two tasks, but there should be a good reason to add complexity to your program.
 

Offline EVblog1Topic starter

  • Regular Contributor
  • *
  • Posts: 145
  • Country: 00
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #7 on: July 11, 2026, 04:02:23 am »
Why do you even need two tasks?

What's wrong with just using blocking reads and writes in a loop, e.g.:

I'm not saying you don't need two tasks, but there should be a good reason to add complexity to your program.

I totally agree with you. For this simple example, a single loop with blocking reads and writes would probably be the simplest and most appropriate solution.

The reason I used two tasks wasn't because I think it's the best implementation for this particular application. My goal was to create a simple RTOS scenario to help me understand when to choose a mutex, semaphore, or queue. I wanted to focus on the communication between two tasks

In a real application, there would usually be a valid reason for having separate tasks. I kept the example intentionally simple so I could better understand the RTOS concepts.
 

Online krish2487

  • Frequent Contributor
  • **
  • Posts: 784
  • Country: dk
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #8 on: July 11, 2026, 06:47:54 am »
Just to complicate things further  :D
and when the 2 tasks turn to 20....
what I have done is to have a private queue (static queue in the source file) to each task with setters to write data to the queue and getters to read data from the queue.
Then I have a "postoffice" queue whose entire job is to read from one task.. and write it to another..
This way, you avoid having global members.. you can synchronize the postoffice task using semaphores or direct to task notifications or mutexes..
each task is fairly self contained and does not care about any other task.. and the update is less prone to having stale data..

This is a design pattern called as an active object. Miro Samek has some good videos on this topic on youtube.
If god made us in his image,
and we are this stupid
then....
 

Online gf

  • Super Contributor
  • ***
  • Posts: 1826
  • Country: de
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #9 on: July 11, 2026, 08:36:36 am »
There is no need for any global data structures for the data. It sits in the queue which acts as a FIFO buffer for the data.
You're saying that using a queue alone is sufficient

My understanding is that both tasks need access to the temperature and humidity data. I was assuming there would be a shared global structure where the Sensor Task writes the temperature and humidity values, and the Terminal Task reads them.

Communicating via a message queue versus shared memory represents two different philosophies. By transferring temperature and humidity data via a queue from one task to another, you avoid shared memory and the need to deal with race conditions upon concurrent access. The producer task sends temperature and humidity data (via queue), and the consumer task receives a copy of the data by value.

[ The message queue implementation, of course, also needs to deal with race conditions, but this happens internally, transparent to the user, so you don't need to worry about it. ]
 
The following users thanked this post: EVblog1

Offline EVblog1Topic starter

  • Regular Contributor
  • *
  • Posts: 145
  • Country: 00
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #10 on: July 11, 2026, 10:15:11 am »
Thank you, everyone, for your replies. Your explanations helped me realize that, in my original scenario, there was no need for a shared global structure because the data could simply be passed directly from the Sensor Task to the Terminal Task using a queue.

Let me slightly modify the scenario to one where I think using both a queue and a mutex would make sense.

A temperature and humidity sensor sends data to the MCU over UART. Each received byte is captured in the UART RX ISR and sent to a Sensor Task using a queue. The Sensor Task reconstructs the complete sensor message, extracts the temperature and humidity values, and stores them in a shared global structure.

A Terminal Task periodically reads this shared global structure and sends the latest temperature and humidity values to a serial terminal over another UART.

My thinking is that a queue is the right choice for safely passing the received bytes from the UART ISR to the Sensor Task. However, because the Sensor Task updates the shared global structure while the Terminal Task reads it, I think a mutex would also be required. Otherwise, the Sensor Task could update the temperature value, get preempted before updating the humidity value, and the Terminal Task could read inconsistent data (for example, a new temperature value with an old humidity value).

Do you agree that using both a queue and a mutex would be the preferred approach for this modified scenario
 

Offline jc101

  • Frequent Contributor
  • **
  • Posts: 871
  • Country: gb
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #11 on: July 11, 2026, 10:15:42 am »
There is no need for any global data structures for the data. It sits in the queue which acts as a FIFO buffer for the data.
You're saying that using a queue alone is sufficient

My understanding is that both tasks need access to the temperature and humidity data. I was assuming there would be a shared global structure where the Sensor Task writes the temperature and humidity values, and the Terminal Task reads them.

If that's the case, couldn't the Sensor Task update the temperature, get preempted before updating the humidity, and then the Terminal Task read the structure? That would result in inconsistent data.

Can you explain reason in detail why a mutex wouldn't be needed in this scenario?

Maybe a very simple example will help. The sensor data is stored not in a global structure but within the queue itself. In this example, up to 5 readings can be stored, which may or may not be required. If data is batched for transmission, the queue acts as the data buffer. The queue size could just as well be 1. The queue holds copies of the data on a FIFO basis. If there is no data to transmit, the terminal task blocks, taking no CPU, until something arrives for it to deal with. The terminal task only outputs a reading when a new reading has been taken. If you wanted to do other stuff in the terminal task, change portMAX_DELAY as required.


Code: [Select]
/*
 * FreeRTOS Queue Example
 * -----------------------
 * Demonstrates passing data between two tasks using a queue:
 *   - SensorTask : reads sensor data and pushes it onto the queue
 *   - TerminalTask: blocks on the queue until data arrives, then "prints" it
 */

#include "FreeRTOS.h"
#include "task.h"
#include "queue.h"

/* ---- Message format sent through the queue ---- */
typedef struct
{
    float temperature;   /* degrees C */
    float humidity;      /* % RH */
} sensor_data_t;

/* ---- Queue configuration ---- */
#define SENSOR_QUEUE_LENGTH    5
#define SENSOR_QUEUE_ITEM_SIZE sizeof(sensor_data_t)

static QueueHandle_t xSensorQueue = NULL;

/* ---- Sensor Task: produces data and sends it to the queue ---- */
static void vSensorTask(void *pvParameters)
{
    sensor_data_t xSensorData;

    for (;;)
    {
        /* Replace with real sensor reads (e.g. I2C/SPI driver calls) */
        xSensorData.temperature = 22.5f;
        xSensorData.humidity    = 45.0f;

        /* Send to the back of the queue, waiting up to 100 ticks
         * if the queue is currently full. */
        if (xQueueSend(xSensorQueue, &xSensorData, pdMS_TO_TICKS(100)) != pdPASS)
        {
            /* Queue full - handle error/drop as appropriate */
        }

        vTaskDelay(pdMS_TO_TICKS(1000)); /* sample once per second */
    }
}

/* ---- Terminal Task: blocks until data is available, then displays it ---- */
static void vTerminalTask(void *pvParameters)
{
    sensor_data_t xReceivedData;

    for (;;)
    {
        /* Block indefinitely until an item is available in the queue */
        if (xQueueReceive(xSensorQueue, &xReceivedData, portMAX_DELAY) == pdPASS)
        {
            /* printf("Temp: %.1f C, Humidity: %.1f %%\n",
             *        xReceivedData.temperature,
             *        xReceivedData.humidity);
             */
        }
    }
}

/* ---- Setup: create queue and tasks, then start scheduler ---- */
int main(void)
{
    xSensorQueue = xQueueCreate(SENSOR_QUEUE_LENGTH, SENSOR_QUEUE_ITEM_SIZE);

    if (xSensorQueue != NULL)
    {
        xTaskCreate(vSensorTask,   "Sensor",   configMINIMAL_STACK_SIZE, NULL, 2, NULL);
        xTaskCreate(vTerminalTask, "Terminal", configMINIMAL_STACK_SIZE, NULL, 1, NULL);

        vTaskStartScheduler();
    }

    /* Should never reach here */
    for (;;) {}
    return 0;
}

You can use a global variable and a mutex, as in the example below. The main difference is the terminal task has no idea if the reading in the global variable is new or not. It is simply looping and testing to see if it can get the mutex and doing stuff with the data. This may or may not be important depending on what you want. If you wanted to only report new data, you would need a flag within the structure the sensor task sets and the terminal class clears when it reads it.

Code: [Select]
/*
 * FreeRTOS Mutex Example
 * -----------------------
 * Demonstrates passing data between two tasks using a mutex-protected
 * global variable instead of a queue:
 *   - SensorTask : reads sensor data and writes it into a shared global,
 *                  guarded by a mutex
 *   - TerminalTask: periodically takes the mutex, reads the shared global,
 *                  and "prints" it
 *
 * Unlike the queue example, this does NOT provide blocking-until-new-data
 * behavior or buffering of multiple readings - the terminal task simply
 * reads whatever the latest value is whenever it looks.
 */

#include "FreeRTOS.h"
#include "task.h"
#include "semphr.h"

/* ---- Shared data format, protected by xSensorMutex ---- */
typedef struct
{
    float temperature;   /* degrees C */
    float humidity;      /* % RH */
} sensor_data_t;

/* ---- Global shared variable and its guarding mutex ---- */
static sensor_data_t xSensorData;
static SemaphoreHandle_t xSensorMutex = NULL;

/* ---- Sensor Task: produces data and writes it under the mutex ---- */
static void vSensorTask(void *pvParameters)
{
    for (;;)
    {
        /* Take the mutex, waiting up to 100 ticks if it's currently held */
        if (xSemaphoreTake(xSensorMutex, pdMS_TO_TICKS(100)) == pdTRUE)
        {
            /* Replace with real sensor reads (e.g. I2C/SPI driver calls) */
            xSensorData.temperature = 22.5f;
            xSensorData.humidity    = 45.0f;

            xSemaphoreGive(xSensorMutex);
        }
        else
        {
            /* Could not get the mutex in time - handle as appropriate */
        }

        vTaskDelay(pdMS_TO_TICKS(1000)); /* sample once per second */
    }
}

/* ---- Terminal Task: periodically reads the shared global under the mutex ---- */
static void vTerminalTask(void *pvParameters)
{
    sensor_data_t xLocalCopy;

    for (;;)
    {
        /* Take the mutex, blocking indefinitely until it's available */
        if (xSemaphoreTake(xSensorMutex, portMAX_DELAY) == pdTRUE)
        {
            /* Copy out while holding the mutex, then release it quickly */
            xLocalCopy = xSensorData;
            xSemaphoreGive(xSensorMutex);

            /* printf("Temp: %.1f C, Humidity: %.1f %%\n",
             *        xLocalCopy.temperature,
             *        xLocalCopy.humidity);
             */
        }

        vTaskDelay(pdMS_TO_TICKS(500)); /* poll twice a second */
    }
}

/* ---- Setup: create mutex and tasks, then start scheduler ---- */
int main(void)
{
    xSensorMutex = xSemaphoreCreateMutex();

    if (xSensorMutex != NULL)
    {
        xTaskCreate(vSensorTask,   "Sensor",   configMINIMAL_STACK_SIZE, NULL, 2, NULL);
        xTaskCreate(vTerminalTask, "Terminal", configMINIMAL_STACK_SIZE, NULL, 1, NULL);

        vTaskStartScheduler();
    }

    /* Should never reach here */
    for (;;) {}
    return 0;
}


To me, a queue is the easiest to use. The terminal only outputs data when there is a new reading and uses no CPU when there is nothing to do. The queue handles everything for me with one function call to put data into the queue and one to remove it. As a queue works by holding a copy of the data, all the data is protected from change within the queue.


 
The following users thanked this post: SteveThackery, EVblog1

Offline jc101

  • Frequent Contributor
  • **
  • Posts: 871
  • Country: gb
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #12 on: July 11, 2026, 10:45:49 am »
Thank you, everyone, for your replies. Your explanations helped me realize that, in my original scenario, there was no need for a shared global structure because the data could simply be passed directly from the Sensor Task to the Terminal Task using a queue.

Let me slightly modify the scenario to one where I think using both a queue and a mutex would make sense.

A temperature and humidity sensor sends data to the MCU over UART. Each received byte is captured in the UART RX ISR and sent to a Sensor Task using a queue. The Sensor Task reconstructs the complete sensor message, extracts the temperature and humidity values, and stores them in a shared global structure.

A Terminal Task periodically reads this shared global structure and sends the latest temperature and humidity values to a serial terminal over another UART.

My thinking is that a queue is the right choice for safely passing the received bytes from the UART ISR to the Sensor Task. However, because the Sensor Task updates the shared global structure while the Terminal Task reads it, I think a mutex would also be required. Otherwise, the Sensor Task could update the temperature value, get preempted before updating the humidity value, and the Terminal Task could read inconsistent data (for example, a new temperature value with an old humidity value).

Do you agree that using both a queue and a mutex would be the preferred approach for this modified scenario

I won't edit my previous post, which I sent just as you put this...

I would use a queue for the UART ISR -> sensor task. If this is FreeRTOS, and you have a single sender and receiver, I'd use a stream buffer. The concept is the same. It's just "lighter" and more efficient behind the scenes.

If the terminal task reports the last received reading at set periods of time, which is different to and not synchronised with when the readings are taken, then a mutex around a global variable is required in your scenario.

The alternative, if that data isn't used anywhere else, is to use a queue from the sensor task to the terminal task, which updates a local copy of the data with a new reading when it arrives. It then reports the local reading whenever it needs to and updates its local copy when new data arrives on the queue. No mutex required, as the update and reporting are within the same task. I would probably do this, as it removes the need to handle the mutex, especially if the tasks are different priorities (read the FreeRTOS docs on priority inheritance). It costs a little more RAM for the sensor -> terminal queue, but I prefer the simplicity.

The queue method is easily extensible. If there were multiple sensors, I could use the same queue for all the different sensor tasks and dequeue them all in the terminal task. Just use a union of all the different sensor types to identity what the sensor is. But that is outside the scope of your question.
« Last Edit: July 11, 2026, 10:48:01 am by jc101 »
 
The following users thanked this post: SteveThackery, EVblog1

Online gf

  • Super Contributor
  • ***
  • Posts: 1826
  • Country: de
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #13 on: July 12, 2026, 05:21:31 pm »
When we consider the scenario where a single producer and one or multiple consumers want to update or read a global struct atomically:

Using a mutex is certainly functional. However, if the payload is remarkably small—such as a few primitive types—directly using taskENTER_CRITICAL() / taskEXIT_CRITICAL() offers a simple, low-latency alternative for single-core architectures, avoiding the non-negligible overhead of a mutex.

Alternatively, a Seqlock provides a completely lock-free mechanism that scales well across both single-core and multi-core systems.
Unfortunately, since FreeRTOS lacks a native Seqlock component, a custom implementation is required.
 

Offline peter-h

  • Super Contributor
  • ***
  • Posts: 5993
  • Country: gb
  • Doing electronics since the 1960s...
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #14 on: July 13, 2026, 12:12:06 pm »
Another approach is that in arm32 you get atomic variables up to and including 4 bytes.

So a data producer can just write a temperature, as a single (not double!) float, into RAM, and any number of RTOS tasks can consume it. You don't need a mutex, queues, or anything.

Somehow I have so far avoided using the FreeRTOS queue facility. But I use mutexes extensively for controlling access to shared hardware. Hardware is obviously not re-entrant ;) I find a mutex takes a few us on a 168MHz 32F4xx, and this is a performance problem sometimes, best solved by doing more stuff inside that mutex protection envelope. For example I am sharing SPI3, so this is mutexed, and re-initialised as required. When driving an LCD, setting a pixel is 11 bytes (set address, and 2 bytes for the pixel), so doing all 11 inside the mutex saves a lot of time (at 42MHz SPI clock) over doing 1 byte at a time. And I optimise long pixel runs by using DMA and then the whole DMA transfer is inside the mutex.

IIRC, the taskENTER_CRITICAL etc merely stops task switching. That will work sometimes, while __disable_irq() gives you atomicity for a negligible overhead.
« Last Edit: July 13, 2026, 12:15:01 pm by peter-h »
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 

Online gf

  • Super Contributor
  • ***
  • Posts: 1826
  • Country: de
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #15 on: July 13, 2026, 08:53:52 pm »
Another approach is that in arm32 you get atomic variables up to and including 4 bytes.
So a data producer can just write a temperature, as a single (not double!) float, into RAM, and any number of RTOS tasks can consume it. You don't need a mutex, queues, or anything.

Exactly. If the processor supports lock-free atomic operations for the payload size, the update boils down to a single atomic_store() and the read simplifies to a standard atomic_load() of the structure. However, as you noted, an atomic structure containing two floats is generally not supported lock-free on typical embedded hardware.

Quote
IIRC, the taskENTER_CRITICAL etc merely stops task switching. That will work sometimes, while __disable_irq() gives you atomicity for a negligible overhead.

You're right about __disable_irq() being even cheaper, but it is ARM-specific (CMSIS). I think the corresponding portable FreeRTOS functions are portDISABLE_INTERRUPTS() and portENABLE_INTERRUPTS().
 

Offline NorthGuy

  • Super Contributor
  • ***
  • Posts: 3519
  • Country: ca
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #16 on: July 13, 2026, 11:33:24 pm »
I don't think there's any need for RTOS here. RTOS would help if you had several complex tasks which you must run in parallel and couldn't do it in a more elegant ways. Such things do not happen very often, hence you rarely need RTOS. So, if you want to use RTOS, get yourself a complex example where it is really needed.

Unfortunately, "learning" often happens the opposite way. People get a simple example which doesn't need much of anything and try to apply whatever they want to learn, RTOS for example. This is often gets exacerbated by academics teachers which perceive this as following a learning curve which has to start from something easy. In the process the pupils fall in love with RTOS (or whatever) and then start to apply the object of their affection everywhere. They may think they're learning RTOS, but in reality they learn how to make simple tasks more complex, which would allow them fully use everything they have learned. Whereas a good engineer does the opposite - he finds simple solutions to complex problems.

 

Offline rf-fil

  • Regular Contributor
  • *
  • Posts: 132
  • Country: au
    • VK2ZJ at QRZ
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #17 on: July 13, 2026, 11:54:13 pm »
I generally tend to avoid mutexes and semaphores. All my RTOS code tends to be structured as a set of tasks that communicate via message queues. Each task is structured as a "reactor" pattern that blocks while waiting for messages. I also try to use separate I2C & SPI busses per external chip, so that I end up with one "driver" task per external chip - so, again, no shared resources and no need for mutexes. Since I've started doing it this way years ago, I've never had any problems with lock-ups, priority inversions, or anything issue like that. My code tends to just work at this level, and I can focus on fixing all my other bugs ;-).
-VK2ZJ
 
The following users thanked this post: tggzzz

Offline uer166

  • Super Contributor
  • ***
  • Posts: 1265
  • Country: us
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #18 on: July 13, 2026, 11:57:09 pm »
Another approach is that in arm32 you get atomic variables up to and including 4 bytes.

So a data producer can just write a temperature, as a single (not double!) float, into RAM, and any number of RTOS tasks can consume it. You don't need a mutex, queues, or anything.

Honestly 98% of my code with different execution contexts (not necessarily RTOS, but same principle) just rely on natural atomicity of variables like temperature, current, voltage, whatever as floats. For the rest I use lock-free FIFOs (I guess queues) because I don't want to introduce jitter with critical sections.

Mutexes are annoying/hard to predict and easily avoided by making a separate task/execution context that is a shared resource gatekeeper/consumer/provider. For example a shared debug UART to which many tasks may wanna print, you simply make ONE task that prints to it, while the rest of the tasks use queues to dump data into the single consumer.
 

Offline uer166

  • Super Contributor
  • ***
  • Posts: 1265
  • Country: us
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #19 on: July 14, 2026, 12:01:18 am »
IIRC, the taskENTER_CRITICAL etc merely stops task switching. That will work sometimes, while __disable_irq() gives you atomicity for a negligible overhead.

No, taskENTER_CRITICAL disables interrupts up to a specific interrupt priority level that is configurable. Those disabled interrupts are then compatible with FreeRTOS's _fromISR APIs (so you can have queues and stuff to/from ISRs), while still being able to have non-disable-able ISRs that are very timing sensitive. For those higher priority ISRs, you cannot use FreeRTOS's APIs to talk to the tasks, though of course the normal 4-byte atomic variables still work totally fine.
 

Offline az1

  • Regular Contributor
  • *
  • Posts: 61
  • Country: us
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #20 on: July 14, 2026, 04:01:26 am »
Another approach is that in arm32 you get atomic variables up to and including 4 bytes.

Unfortunately ARM v6m doesn't have ldrex/strex which rules out M0/M0+. Just for fun some M23 and M33 cores don't either.
 

Offline uer166

  • Super Contributor
  • ***
  • Posts: 1265
  • Country: us
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #21 on: July 14, 2026, 04:44:51 am »
Another approach is that in arm32 you get atomic variables up to and including 4 bytes.

Unfortunately ARM v6m doesn't have ldrex/strex which rules out M0/M0+. Just for fun some M23 and M33 cores don't either.

A regular old load/store is guaranteed to be atomic on 32-bit ARM when aligned such as M0/M4/M33, etc. Exclusive load/stores are there to enable atomic RMW operations, not to allow atomic writes or reads on their own.

For example an ISR or task producing a 32-bit float and another ISR/task consuming the 32-bit float is a perfectly sane operation using LDR/STR on in-order CPUs we're talking about.
 
The following users thanked this post: voltsandjolts

Online gf

  • Super Contributor
  • ***
  • Posts: 1826
  • Country: de
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #22 on: July 14, 2026, 08:24:59 am »
A regular old load/store is guaranteed to be atomic on 32-bit ARM when aligned such as M0/M4/M33, etc.

That holds true at the ARM machine level. However, the abstract C memory model only provides atomicity guarantees for variables qualified with _Atomic. Strictly speaking, to perform an atomic store or load while avoiding heavy hardware synchronization barriers, you had to write:

Code: [Select]
_Atomic int counter = 0;
...
atomic_store_explicit(&counter, 5, memory_order_relaxed);

Without _Atomic, the compiler would still be allowed to emit e.g. four STRB instructions for  counter = 5, which would result in byte tearing and no longer be atomic. This explicit syntax guarantees a single, undivided 32-bit STR instruction (on ARM, or whatever equivalent instruction is required for the target architecture).

While developers often assume a compiler would never split a standard 32-bit assignment, relying on this behavior makes the code compiler- and machine-dependent and non-portable. Without _Atomic, the compiler has the legal right to alter the generated instructions, forcing us to rely on implementation artifacts rather than the official language standard.

Furthermore, since lock-free atomics are not supported for every size on every CPU, one should check the capability explicitly. For several trivial types, there are ATOMIC_xxx_LOCK_FREE macros available. For more complex types, one can verify lock-free status at runtime, e.g.:

Code: [Select]
_Atomic struct MyData my_var;

if (!atomic_is_lock_free(&my_var)) {
    configASSERT(0);
}

Unfortunately, atomic_is_lock_free() is a runtime check (it is not a compile-time constant expression). While GCC and Clang enable compile-time verification via the __atomic_always_lock_free() built-in, it remains an extension and is not part of standard C.
 

Offline az1

  • Regular Contributor
  • *
  • Posts: 61
  • Country: us
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #23 on: July 14, 2026, 08:35:10 am »
A regular old load/store is guaranteed to be atomic on 32-bit ARM when aligned such as M0/M4/M33, etc.

That holds true at the ARM machine level. However, the abstract C memory model only provides atomicity guarantees for variables qualified with _Atomic.

Specifically: clang doesn't provide _atomic_* functions on armv6 and if e.g. atomic_store uses strex behind the scenes it will behave unexpectedly on the oddball M23/M33 cores.  A quick read through of the datasheet and errata can save a ton of time debugging here.
 
The following users thanked this post: mikerj

Offline peter-h

  • Super Contributor
  • ***
  • Posts: 5993
  • Country: gb
  • Doing electronics since the 1960s...
Re: Choosing Between Mutex, Semaphore, and Queue in RTOS
« Reply #24 on: July 14, 2026, 09:35:38 am »
I think an RTOS is the best thing since sliced bread (to use an English expression) and even in the 1980s I wrote my own (Z180,Z280) basic ones. It is just a super efficient way to do "real time" software.

The FreeRTOS mutex code is fairly convoluted. I am not sure why. One could achieve the same thing with a test/set instruction which I believe the arm32 has. The Z280 had it and the overhead is basically nothing. On others you have to disable interrupts to do the same thing.

I've avoided using queues. AFAICT FreeRTOS allocates space for these on an internal heap. This should be safe since the malloced RAM is never freed. But I've not seen a need. It is an attractive thing in terms of "architecture" but is rarely needed.

I have used FreeRTOS timers for various things. They work well.

On 32F4, are <= 4 byte variables not atomic if not aligned? AFAIK the compiler aligns most of them anyway...
Z80 Z180 Z280 Z8 S8 8031 8051 H8/300 H8/500 80x86 90S1200 32F417
 


Share me

Digg  Facebook  SlashDot  Delicious  Technorati  Twitter  Google  Yahoo
Smf