Aller au contenu

Les Mutex

Avec FreeRTOS, les mutex sont créés avec la fonction xSemaphoreCreateMutex :

SemaphoreHandle_t xSemaphoreCreateMutex(void)

Comme pour les sémaphores, les fonctions xSemaphoreTake et xSemaphoreGive permettent de prendre (décrémenter) respectivement de donner (incrémenter) un mutex.

Avec FreeRTOS, un mutex peut aussi être récursif. Ça signifie que si une tâche a obtenu un mutex, elle peut le reprendre une nouvelle fois sans se faire bloquer grâce à la fonction.

Pour être récursif, le mutex doit être créé avec la fonction xSemaphoreCreateRecursiveMutex:

SemaphoreHandle_t xSemaphoreCreateRecursiveMutex(void)

La fonction xSemaphoreTakeRecursive permet de décrémenter un mutex récursif:

xSemaphoreTakeRecursive(SemaphoreHandle_t xMutex, TickType_t xTicksToWait);

Exemple :

SemaphoreHandle_t xMutex = NULL;

// A task that creates a mutex.
void vATask(void* pvParameters) {
    // Create the mutex to guard a shared resource.
    xMutex = xSemaphoreCreateRecursiveMutex();
}

// A task that uses the mutex.
void vAnotherTask(void* pvParameters) {
    // ... Do other things.

    if (xMutex != NULL) {
        // See if we can obtain the mutex.  If the mutex is not available
        // wait 10 ticks to see if it becomes free.
        if (xSemaphoreTakeRecursive(xMutex, (TickType_t)10) == pdTRUE) {
            // We were able to obtain the mutex and can now access the
            // shared resource.

            // ...
            // For some reason due to the nature of the code further calls to
            // xSemaphoreTakeRecursive() are made on the same mutex.  In real
            // code these would not be just sequential calls as this would make
            // no sense.  Instead the calls are likely to be buried inside
            // a more complex call structure.
            xSemaphoreTakeRecursive(xMutex, (TickType_t)10);
            xSemaphoreTakeRecursive(xMutex, (TickType_t)10);

            // The mutex has now been 'taken' three times, so will not be
            // available to another task until it has also been given back
            // three times.  Again it is unlikely that real code would have
            // these calls sequentially, but instead buried in a more complex
            // call structure.  This is just for illustrative purposes.
            xSemaphoreGiveRecursive(xMutex);
            xSemaphoreGiveRecursive(xMutex);
            xSemaphoreGiveRecursive(xMutex);

            // Now the mutex can be taken by other tasks.
        } else {
            // We could not obtain the mutex and can therefore not access
            // the shared resource safely.
        }
    }
}

La fonction xSemaphoreGiveRecursive permet d’incrémenter un mutex récursif:

xSemaphoreGiveRecursive(SemaphoreHandle_t xMutex)

Exemple :

SemaphoreHandle_t xMutex = NULL;

// A task that creates a mutex.
void vATask(void* pvParameters) {
    // Create the mutex to guard a shared resource.
    xMutex = xSemaphoreCreateRecursiveMutex();
}

// A task that uses the mutex.
void vAnotherTask(void* pvParameters) {
    // ... Do other things.

    if (xMutex != NULL) {
        // See if we can obtain the mutex.  If the mutex is not available
        // wait 10 ticks to see if it becomes free.
        if (xSemaphoreTakeRecursive(xMutex, (TickType_t)10) == pdTRUE) {
            // We were able to obtain the mutex and can now access the
            // shared resource.

            // ...
            // For some reason due to the nature of the code further calls to
            // xSemaphoreTakeRecursive() are made on the same mutex.  In real
            // code these would not be just sequential calls as this would make
            // no sense.  Instead the calls are likely to be buried inside
            // a more complex call structure.
            xSemaphoreTakeRecursive(xMutex, (TickType_t)10);
            xSemaphoreTakeRecursive(xMutex, (TickType_t)10);

            // The mutex has now been 'taken' three times, so will not be
            // available to another task until it has also been given back
            // three times.  Again it is unlikely that real code would have
            // these calls sequentially, it would be more likely that the calls
            // to xSemaphoreGiveRecursive() would be called as a call stack
            // unwound.  This is just for demonstrative purposes.
            xSemaphoreGiveRecursive(xMutex);
            xSemaphoreGiveRecursive(xMutex);
            xSemaphoreGiveRecursive(xMutex);

            // Now the mutex can be taken by other tasks.
        } else {
            // We could not obtain the mutex and can therefore not access
            // the shared resource safely.
        }
    }
}

Est-ce que les mutex sont des sémaphores binaires ?

Une remarque très importante concernant les sémaphores et les mutex est qu’il est faux de considérer un mutex comme étant un sémaphore binaire. Contrairement au mutex pour laquelle on peut dire qu’elle a un propriétaire (la tâche qui fait un Take() sur le mutex), un sémaphore n’a pas de propriétaire. Un mutex est un mécanisme de protection (lock) alors qu’un sémaphore est un mécanisme de signalisation. En d’autres termes, alors qu’un mutex est toujours verrouillé puis déverrouillé (dans cet ordre) par la même tâche, les tâches utilisant un sémaphore vont soit la signaler (appel à Give()) ou attendre (appel à Take()), en fonction de leur rôle.

Une autre différence importante est que les mutex ne peuvent pas être utilisés dans un contexte ISR, au contraire des sémaphores qui peuvent être signalés ou attendus de façon non bloquante (avec la méthode xSemaphoreTakeFromISR() par exemple). Les sémaphores peuvent donc servir à signaler des threads en attente depuis un contexte ISR.

Il est encore utile de mentionner qu’au contraire des sémaphores, les mutex peuvent être récursifs (ré-entrants) avec FreeRTOS. Cela signifie qu’un mutex peut être acquis plusieurs fois par la même tâche, sans créer de deadlock. Il est utile de démontrer ce point pour une classe utilisant un mécanisme de composition avec une classe réalisant un mécanisme de verrouillage:

class A {
    void Lock() { xSemaphoreTake(mutex_, (TickType_t)0); }

    void Unlock() { xSemaphoreGive(mutex_); }

   private:
    SemaphoreHandle_t mutex_;
};

class B {
    void Method1() {
        a_.Lock();
        // ...
        Method2();
        // ...
        a_.Unlock();
    }

    void Method2() {
        a_.Lock();
        // ...
        a_.Unlock();
    }

   private:
    A a_;  // B owns an instance of A
};

Exercice Les Mutex/1

Dans l’exemple ci-dessus, il est admis que les méthodes Method1() et Method2()de la classe B doivent acquérir l’instance de A afin d’effectuer un ensemble d’opérations qui doivent être atomiques sur cette instance. Expliquer pourquoi un deadlock survient si le mutex n’est pas ré-entrant.

Solution

Dans Method1(), a_ est acquis puis Method2() est appelée. Dans Method2(), a_ est acquis à nouveau. Si le mutex n’est pas ré-entrant, alors un deadlock survient.

Vous trouverez encore un complément d’information afin de comparer mutex et sémaphores dans cet article.