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.