5

I'm experimenting with locking on an ESP32. Apparently, there are different ways to implement a lock:

  1. There is the default C++ mutex library:

    #include <mutex>
    
    std::mutex mtx;
    
    mtx.lock();
    
    mtx.unlock();
    
  2. And there is the implementation from RTOS:

    SemaphoreHandle_t xMutex = xSemaphoreCreateMutex();
    
    xSemaphoreTake(xMutex, portMAX_DELAY);
    
    xSemaphoreGive(xMutex);
    

Are there fundamental differences I should be aware of? Or are they equivalent?

Falko
  • 17,076
  • 13
  • 60
  • 105
  • 1
    Without looking at the code for the standard C++ library on your system, it's impossible to say how it is actually implemented - the C++ standard specifies how it behaves as far as a program is concerned, not how it is implemented. The main practical difference between the two is that the first works on all implementations (combination of host system, compiler, and standard library) of C++11 and later unless the implementation has a bug, whereas the second only works on particular implementations, but not others. – Peter Aug 09 '19 at 12:42
  • Those are not different ways to _implement_ a lock. Those are different library APIs. The implementations are in the library routines that your code calls. – Solomon Slow Aug 09 '19 at 13:30
  • 1
    Are you using the ESP-IDF SDK or something else? – rustyx Aug 09 '19 at 15:39
  • @rustyx Not necessarily. I'm using the [Arduino core](https://github.com/espressif/arduino-esp32) as well. – Falko Aug 09 '19 at 17:06

2 Answers2

4

Assuming you're using the ESP-IDF SDK, the toolchain is based on GCC 5.2 targeting the xtensa-lx106 instruction set, with a partially open-source C runtime library.

std::mutex in GNU libstdc++ delegates to pthread_mutex_lock/unlock calls. ESP-IDF SDK contains a pthread emulation layer, where we can see what pthread_mutex_lock and pthread_mutex_unlock actually do:

static int IRAM_ATTR pthread_mutex_lock_internal(esp_pthread_mutex_t *mux, TickType_t tmo)
{
    if (!mux) {
        return EINVAL;
    }

    if ((mux->type == PTHREAD_MUTEX_ERRORCHECK) &&
        (xSemaphoreGetMutexHolder(mux->sem) == xTaskGetCurrentTaskHandle())) {
        return EDEADLK;
    }

    if (mux->type == PTHREAD_MUTEX_RECURSIVE) {
        if (xSemaphoreTakeRecursive(mux->sem, tmo) != pdTRUE) {
            return EBUSY;
        }
    } else {
        if (xSemaphoreTake(mux->sem, tmo) != pdTRUE) {
            return EBUSY;
        }
    }

    return 0;
}

int IRAM_ATTR pthread_mutex_unlock(pthread_mutex_t *mutex)
{
    esp_pthread_mutex_t *mux;

    if (!mutex) {
        return EINVAL;
    }
    mux = (esp_pthread_mutex_t *)*mutex;
    if (!mux) {
        return EINVAL;
    }

    if (((mux->type == PTHREAD_MUTEX_RECURSIVE) ||
        (mux->type == PTHREAD_MUTEX_ERRORCHECK)) &&
        (xSemaphoreGetMutexHolder(mux->sem) != xTaskGetCurrentTaskHandle())) {
        return EPERM;
    }

    int ret;
    if (mux->type == PTHREAD_MUTEX_RECURSIVE) {
        ret = xSemaphoreGiveRecursive(mux->sem);
    } else {
        ret = xSemaphoreGive(mux->sem);
    }
    if (ret != pdTRUE) {
        assert(false && "Failed to unlock mutex!");
    }
    return 0;
}

So as you can see it mainly delegates the calls to the RTOS semaphore API, with some additional checks.

Chances are you don't need/want those checks. Given the tiny i-cache of the esp32 chip and the excruciatingly slow serial RAM, I would prefer to stay as close to the hardware as possible (i.e. don't use std::mutex unless it does exactly what you need).

rustyx
  • 80,671
  • 25
  • 200
  • 267
  • Great! That's exactly what I was looking for. Thanks! :) – Falko Aug 09 '19 at 17:03
  • 1
    Very nice and detailed explanation but I would not agree with the final conclusion in general: sticking with higher level mechanics might introduce overhead but premature optimization can be very dangerous as we all know. So as long as one has not identified the high level implementation as bottleneck I would personally recommend to use it instead of bare metal works. – Christian B. Aug 11 '19 at 09:00
1

Are there fundamental differences I should be aware of?

I am not familiar with the API that you're calling in your second example, but it looks as if your xMutex variable refers to a counting semaphore. The "semaphore" abstraction is more powerful than the "mutex" abstraction. I.e., you can always use a semaphore as a substitute for a mutex, but there are some algorithms in which a mutex would not work as a substitute for a semaphore.

I like to think of a semaphore as a blocking queue of informationless tokens. The "give" operation puts a token into the queue, while the "take" takes one from the queue, possibly waiting for some other thread to give a token if the queue happens to be empty at the moment when take() was called.


P.S., In order to use a semaphore as a substitute for a mutex, you'll need it to contain one token when the mutex should be "free", and zero tokens when the mutex should be "in use." That means, you'll want the code that creates the semaphore to ensure that it contains one token at the start

The xMutex = xSemaphoreCreateMutex() statement in your example does not explicitly show how many tokens the new semaphore contains. If it's zero tokens, then you'll probably want your next line of code to "give()" one token in order to complete the initialization.

Solomon Slow
  • 25,130
  • 5
  • 37
  • 57