How to use mutex

I have ESP32 i am trying to understand mutex in freertos. https://www.freertos.org/a00113.html

I have created two tasks and I want to use mutex

void setup()
{
  Serial.begin(112500);
  /* Create two tasks. */
  xTaskCreate( vTask1, "Task 1", 10000, NULL, 1, NULL); 
  xTaskCreate( vTask2, "Task 2", 10000, NULL, 1, NULL); 
}


void loop() {
  // Do nothing

}

void vTask1( void *pvParameters )
{
  /* As per most tasks, this task is implemented in an infinite loop. */
  for(;;)
  {
    Serial.println("Task 1 is running");
   
   }
}

void vTask2( void *pvParameters )
{
  
  for(;;)
  {
    Serial.println("Task 2 is running");
  
   }
}

How to use mutex in code ?

Just use a std::mutex:

#include <mutex>

std::mutex serial_mtx;

void setup() {
  Serial.begin(112500);
  xTaskCreate(vTask1, "Task 1", 10000, NULL, 1, NULL);
  xTaskCreate(vTask2, "Task 2", 10000, NULL, 1, NULL);
}

void loop() {}

void vTask1(void *pvParameters) {
  for (;;) {
    std::lock_guard<std::mutex> lck(serial_mtx);
    Serial.println("Task 1 is running");
  }
}

void vTask2(void *pvParameters) {
  for (;;) {
    std::lock_guard<std::mutex> lck(serial_mtx);
    Serial.println("Task 2 is running");
  }
}
1 Like

I think mutex needs to be used When two tasks are sharing the same resource.

What the resource is task's sharing in your project ?

Why would you use a semaphore to protect an integer? Either use the semaphore directly, or use a plain atomic integer.

Either way, the semaphore is pointless because you have unprotected reads from the shared variable:

Even if you can load and store an integer in a single instruction, it's still a race condition, so the code would be invalid. And there's also no guarantee that addition or increment is carried out atomically.

There are two important things std::atomic takes care of:

  1. Atomicity of operations, e.g. no partial reads/writes, correct read-modify-write operations like increment/decrement.
    If the loads/stores for e.g. 32-bit words on a specific system are already atomic, or if the architecture has an atomic increment instruction, no additional instructions have to be generated, but if the system doesn't support this (e.g. 64-bit doubles might not be loaded/stored atomically on some systems), the compiler will insert the necessary locks to ensure atomicity.
  2. Memory order: modern systems have out of order execution and write buffers, consequently, there is no total global order of operations that all cores/threads agree upon. If you need to communicate between threads and you want changes to multiple variables to become visible in a certain order, you need appropriate memory barriers, which std::atomic will take care of. These memory barriers are also needed at compile time, to prevent the compiler from reordering instructions.
    Mutexes and semaphores are basically atomic integers with special names for the increment and decrement operators, and they also emit the necessary memory barriers to guarantee acquire/release semantics between cores.

That's exactly the problem. If you read and write to a shared variable from multiple threads, you have to protect all accesses, including reads. The read I quoted is not protected by the mutex, so you have a race condition and your code is invalid. See Memory model - cppreference.com

The optimizer doesn't know or care, a race condition is a race condition, so you lose all guarantees about the correctness of your program, not just the correctness of that specific line of code.

Besides, the read is always executed, even if the branch is not taken.

Cool.

When i have two task's I don't understand how to use following API

xSemaphoreGive
xSemaphoreTake

Could you specify what it is exactly that you don't understand?

This code:

// Create a mutex:
std::mutex mutex;
// Use the mutex in a task:
{
  std::lock_guard<std::mutex> lck(mutex); // enter critical section
  /* access shared resources */
} // automatically exit critical section

is equivalent to

// Create a mutex:
SemaphoreHandle_t mutex = xSemaphoreCreateMutex();
assert(mutex);
// Use the mutex in a task:
{
  xSemaphoreTake(mutex, portMAX_DELAY); // enter critical section
  /* access shared resources */
  xSemaphoreGive(mutex); // exit critical section
}
// Must manually destroy the mutex when no longer needed:
vSemaphoreDelete(mutex);

I am trying two understand how to use API xSemaphoreGive and xSemaphoreTake in the code

#include <mutex>

SemaphoreHandle_t mutex = xSemaphoreCreateMutex();

void setup() {

  assert(mutex);
  Serial.begin(112500);
  xTaskCreate(vTask1, "Task 1", 10000, NULL, 1, NULL);
  xTaskCreate(vTask2, "Task 2", 10000, NULL, 1, NULL);
}

void loop() {}

void vTask1(void *pvParameters) {
  for (;;) {
   xSemaphoreTake(mutex, portMAX_DELAY); // enter critical section
    Serial.println("Task 1 is running");
    xSemaphoreGive(mutex); // exit critical section
  }
}

void vTask2(void *pvParameters) {
  for (;;) {
    xSemaphoreTake(mutex, portMAX_DELAY); // enter critical section
    Serial.println("Task 2 is running");
     xSemaphoreGive(mutex); // exit critical section
  }
}

Am I using both APIs correctly for mutex ? What is portMAX_DELAY ?

1 Like

That looks alright.

From https://www.freertos.org/a00122.html :

xTicksToWaitThe time in ticks to wait for the semaphore to become available. The macro portTICK_PERIOD_MS can be used to convert this to a real time. A block time of zero can be used to poll the semaphore.

If INCLUDE_vTaskSuspend is set to '1' then specifying the block time as portMAX_DELAY will cause the task to block indefinitely (without a timeout).

i have esp32, breadboard LED's , According to you, What would be simple example of mutex, which can be verified by experimenting. I just want to see what would be disadvantage of not using mutex

The disadvantage is that your code is just incorrect if you try to concurrently access a shared resource without the necessary protection (e.g. a mutex). Data races are undefined behavior, if you have it in your code, the compiler can in theory produce any nonsense imaginable. In practice, your code will could just crash catastrophically at runtime because some invariants no longer valid or memory gets corrupted, or it might just occasionally produce invalid results, it's impossible to tell.

You don't even need any special hardware to test this, try this on your computer, for example:

#include <mutex>
#include <string>
#include <thread>
#include <iostream>

std::string unprotected = "";

void task1() {
    for (int i = 0; i < 100; ++i)
        unprotected += '1';
}
void task2() {
    for (int i = 0; i < 100; ++i)
        unprotected += '2';
}

int main() {
    std::thread t1{task1};
    std::thread t2{task2};
    t1.join();
    t2.join();
    std::cout << unprotected.length() << ": " << unprotected << std::endl;
}

The result contained anywhere between 100 and 200 characters, and for me it often just crashes with the message free(): invalid pointer because the std::string data is completely messed up because of the data races.

The solution in this case is to use a mutex to protect all concurrent accesses to the shared variable:

std::string unprotected = "";
std::mutex mtx;

void task1() {
    for (int i = 0; i < 100; ++i) {
        std::lock_guard<std::mutex> lck(mtx);
        unprotected += '1';
    }
}
void task2() {
    for (int i = 0; i < 100; ++i) {
        std::lock_guard<std::mutex> lck(mtx);
        unprotected += '2';
    }
}

Even though the order of the '1's and '2's in the string is still nondeterministic, you will now always get a string of length 200 and it will never crash.


Since bugs like can be very sporadic and hard to track down in some cases, tools like the Thread Sanitizer can help you at runtime, see https://godbolt.org/z/6qxM898z1.

For the first snippet above, it produces the following output:

WARNING: ThreadSanitizer: data race (pid=1)
  Read of size 8 at 0x0000004052a8 by thread T2:
    #0 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::size() const /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/basic_string.h:920 (output.s+0x4026ef)
    #1 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::push_back(char) /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/basic_string.h:1342 (output.s+0x4026ef)
    #2 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator+=(char) /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/basic_string.h:1179 (output.s+0x4026ef)
    #3 task2() /app/example.cpp:18 (output.s+0x4026ef)
    ...

  Previous write of size 8 at 0x0000004052a8 by thread T1:
    #0 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_length(unsigned long) /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/basic_string.h:191 (output.s+0x4025b8)
    #1 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::_M_set_length(unsigned long) /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/basic_string.h:224 (output.s+0x4025b8)
    #2 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::push_back(char) /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/basic_string.h:1346 (output.s+0x4025b8)
    #3 std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::operator+=(char) /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/basic_string.h:1179 (output.s+0x4025b8)
    #4 task1() /app/example.cpp:12 (output.s+0x4025b8)
    ...

  Location is global 'unprotected[abi:cxx11]' of size 32 at 0x0000004052a0 (output.s+0x0000004052a8)

  Thread T2 (tid=4, running) created by main thread at:
    #0 pthread_create <null> (libtsan.so.0+0x5fdc5)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xdbb29)
    #2 main /app/example.cpp:24 (output.s+0x40227d)

  Thread T1 (tid=3, finished) created by main thread at:
    #0 pthread_create <null> (libtsan.so.0+0x5fdc5)
    #1 std::thread::_M_start_thread(std::unique_ptr<std::thread::_State, std::default_delete<std::thread::_State> >, void (*)()) <null> (libstdc++.so.6+0xdbb29)
    #2 main /app/example.cpp:23 (output.s+0x40226e)

SUMMARY: ThreadSanitizer: data race /opt/compiler-explorer/gcc-11.2.0/include/c++/11.2.0/bits/basic_string.h:920 in std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >::size() const

I still don't fully understand, Let’s say I have several I2C Devices on a single bus. Now in which condition mutex will used and in which condition it will not use ?

std::mutex and FreeRTOS mutexes can be used to provide mutually exclusive access to resources that are shared between threads of execution.
Such mutexes are not used for sharing a bus with multiple devices, that's something that the I²C protocol itself takes care of.

When using mutexes in programming, they are often used to protect shared variables and data structures, as in the example I posted earlier.

Do you understand why you need a mutex in my previous example?

Re: I2C: Let's say if you have two tasks that need I²C communication to do their job. Since there is only a single I²C peripheral connected to the bus, this I²C hardware and the bus is a shared resource. This means that you now have to protect this shared resource using a mutex:

  • When a task wants to start communicating with the I²C hardware, it first has to acquire the mutex.
  • If it succeeded acquiring the mutex, it now has exclusive access to the I²C interface.
  • The tasks performs its duties, communicating with an I²C device.
  • If at this point in time another task wants to do I²C communication, it will try to acquire the mutex, and fail, because the first task still holds the mutex. This second task will have to wait until the mutex is free again.
  • When the first task is ready, it releases the mutex.
  • After releasing the mutex, the first task is no longer allowed to access the I²C hardware (unless it acquires the mutex again).
  • If another task was waiting, it can now acquire the mutex because the first task released it.

Note that in practice you probably shouldn't do this for I²C communication. Instead, you create a specific task whose only job it is to communicate with the I²C hardware. All other tasks can then pass messages to the I²C task, which will then do the actual I²C communication on their behalf.

(In fact, I think you might not even be able to access the I²C hardware from different cores because of the way the interrupt controller of the ESP32 is configured, but either way, it's a bad idea.)

1 Like

This topic was automatically closed 180 days after the last reply. New replies are no longer allowed.