Exercices du chapitre "Modèles de programmation"
Attentes actives
Exercice 1
Pourquoi n’est-il pas souhaitable d’effectuer des attentes actives (qui occupent le CPU) ?
Solution
- Une attente active empêchera tout d’abord le CPU d’entrer en mode d’économie d’énergie. Comme présenté dans le chapitre précédent, les processeurs Cortex-M sont dotés de la possibilité de gérer leur consommation avec différents Sleep modes. Faire travailler le CPU pour attendre empêche l’activation de tout mode Sleep.
- Dans le cadre d’une application multi-tâches et en fonction des différents algorithmes d’ordonnancement, si une tâche doit attendre il est alors préférable d’attendre à l’aide d’un
Timer
et de donner l’opportunité à l’ordonnanceur de planifier une autre tâche.
Ordonnancement de tâches périodiques
Exercice 2
Soit un programme qui contient deux tâches périodiques avec les spécifications temporelles suivantes: C1=400ms, D1=500ms, T1=1000ms, C2=500ms, D2 = 1000ms, T2=1000ms. Esquissez le diagramme temporel d’une période complète de 1000ms, en indiquant toutes les caractéristiques temporelles (C, T, a, s, f, D) pour les deux tâches.
Solution
Super-loop et ordonnancement manuel
Exercice 3
Vous devez établir la table des tâches pour le problème ci-dessous et réalisez le programme à l’aide d’un modèle super-loop.
Votre programme comporte deux tâches avec les paramètres T1=1000ms, C1=50ms, T2=1500ms et C2=50ms. La tâche consiste à inverser une LED donnée et le code de la fonction réalisant la tâche est
void task(DigitalOut& led) {
led = !led;
wait_us(50000);
}
wait_us
est utilisée afin de simuler un temps de calcul C=50ms.
Etablissez la table des tâches et réalisez le programme à l’aide d’une super boucle. Si la table des tâches comporte des temps d’attente, vous devez réaliser ces temps d’attente en appelant la méthode ThisThread::sleep_for()
, qui au contraire de wait_us
réalise une attente passive et permet au système d’éventuellement accomplir d’autres tâches.
Solution
#include "mbed.h"
static constexpr int kLedOn = 0;
static constexpr int kLedOff = 1;
void task(DigitalOut& led) {
led = !led;
wait_us(50000);
}
int main()
{
DigitalOut led1(LED1, kLedOff);
DigitalOut led2(LED2, kLedOff);
while (true) {
task(led1);
ThisThread::sleep_for(350ms);
task(led2);
ThisThread::sleep_for(550ms);
task(led1);
ThisThread::sleep_for(850ms);
task(led2);
ThisThread::sleep_for(50ms);
task(led1);
ThisThread::sleep_for(950ms);
}
}
Ordonnancement statique cyclique
Exercice 4
Trouvez au moins un avantage et un inconvénient supplémentaire inhérent à un système réalisant un ordonnancement statique cyclique.
Solution
Avantages:
- un ordonnanceur n’est pas requis et l’overhead lié à son utilisation est ainsi supprimé (temps de calcul, context switch).
Inconvénients:
- si les résultats d’une tâche dépendent de conditions externes, le délai maximal permettant de traiter la tâche avec de nouvelles conditions externes est le cycle de répétition de la tâche. Un exemple pour un tel délai est une tâche qui doit être effectuée en tenant compte de la valeur obtenue d’un capteur.
- la scalabilité d’un tel système n’est pas optimale. Ajouter une nouvelle tâche est complexe et devient de plus en plus complexe avec chaque tâche ajoutée.
Détection d’évenement par polling
Exercice 5
Analyser le programme ci-dessous. Que peut-il se passer dans l’exécution du programme suivant concernant le traitement de la pression du bouton?
#include "mbed.h"
int main()
{
DigitalOut led1(LED1);
DigitalIn button(PA_0);
while (true) {
ThisThread::sleep_for(2s);
if (button)
{
led1 = !led1;
}
}
}
Solution
Si la pression sur le bouton ne dure pas assez longtemps, alors l’événement ne sera pas détecté pas le programme.
Détection d’évenement par interruption
Exercice Exercices du chapitre “Modèles de programmation”/6
Modifier l’exemple de code de l’exercice 5 afin que la pression sur le bouton soit traitée selon le principe d’interruption. Observez la différence de comportement de l’application entre les deux solutions.
Solution
#include "mbed.h"
static constexpr int kLedOn = 0;
static constexpr int kLedOff = 1;
volatile bool buttonPressed = false;
void buttonISR() {
buttonPressed = true;
}
int main()
{
DigitalOut led1(LED1, kLedOn);
InterruptIn button(PA_0);
button.fall(&buttonISR);
while (true) {
ThisThread::sleep_for(2s);
if (buttonPressed)
{
led1 = !led1;
buttonPressed = false;
}
}
}
Machine d’état
Exercice 7
Considérons le code que nous avons écrit dans le chapitre Machines d’état pour faire clignoter les LEDs. Si on remplace le programme principal par le suivant :
int main()
{
System system;
system.AppendStateMachine(new Led(LED1, 60));
system.AppendStateMachine(new Led(LED2, 45));
system.AppendStateMachine(new Led(LED3, 30));
system.AppendStateMachine(new Led(LED4, 15));
Ticker clock;
clock.attach(callback(&system, &System::Tick), 15ms);
while (true) {
ThisThread::sleep_for(Kernel::wait_for_u32_forever);
}
}
Quelle est la période de clignotement de chacune des 4 LEDs ?
Ce programme n’est pas optimal. Identifiez sa principale faiblesse et corrigez-la.
Solution
- La LED1 change d’état toutes les \(60 \cdot 15\,ms = 900\,ms\). La période est donc de \(900\,ms\).
- La période de la LED2 est de \(45 \cdot 15 = 675\,ms\).
- La période de la LED3 est de \(30 \cdot 15 = 450\,ms\).
- La période de la LED4 est de \(15 \cdot 15 = 225\,ms\).
Le plus grand diviseur commun n’est pas \(15\), mais \(15 \cdot 15 = 225\,ms\). On pourrait donc mettre le programme en veille plus longtemps avec le code suivant :
int main2()
{
System system;
system.AppendStateMachine(new Led(LED1, 4));
system.AppendStateMachine(new Led(LED2, 3));
system.AppendStateMachine(new Led(LED3, 2));
system.AppendStateMachine(new Led(LED4, 1));
Ticker clock;
clock.attach(callback(&system, &System::Tick), 225ms);
while (true) {
ThisThread::sleep_for(Kernel::wait_for_u32_forever);
}
}
Race condition
Exercice 8
Imaginez que l’exécution de la méthode getAndPrintDateTime
soit
interrompue alors que les valeurs de la variable locale dt
ont été
partiellement mises à jour. Dans ce cas, décrivez un problème qui
pourrait mener à une mise à jour erronée de l’horloge.
Il suffit d’ajouter une attente active de 5ms
à un endroit donné
afin de reproduire le problème de manière systématique: décrivez
ce changement.
Solution
Le problème qui peut survenir est le suivant:
- Supposons que le temps courant soit
day: 0, hour: 10, minute: 59, second: 59
- Le premier appel à la méthode
getAndPrintDateTime
est effectué par le thread principal par l’intermédiaire d’un événement sur l’instance deEventQueue
clockDisplayQueue_
. Supposons que les deux premières instructions C++ soient exécutées et que les valeurs{0, 10}
soient donc copiées dansdt.day/hour
. - Supposons qu’à ce moment-là, un changement de contexte soit ordonné par le
système et que le thread mettant à jour l’horloge soit exécuté.
Le nouveau temps sera donc
day: 0, hour: 11, minute: 0, second: 0
. - Plus tard, la méthode
getAndPrintDateTime
reprend son cours d’exécution et copie donc les champs suivants dans la variabledt
(copiant donc{0, 0}
dans les champsdt.minute/second
). La valeur des champs de la variabledt
sont donc{0, 10, 0, 0}
. Le temps a donc reculé de 1h !
La probabilité de l’occurrence d’un tel événement est bien sûr très faible.
Mais il y a un moyen simple de le provoquer: ajouter un appel à une fonction
d’attente active entre la modification de dt.hour
et de dt.minute
,
de la façon suivante:
dt.hour = m_currentTime.hour;
wait_us(5000);
dt.minute = m_currentTime.minute;
En forçant une attente et en sachant que le Quantum de l’ordonnanceur
round-robin de Mbed OS est de 5 ms
(voir RTX System
Configuration
et RTX_Config.h),
une attente de 5 ms
génère à chaque fois un changement de contexte
et permet ainsi de reproduire l’erreur systématiquement.
Race condition et mutex
Exercice 9
Modifiez la réalisation de la classe Clock
afin de protéger les
accès concurrents aux sections critiques à l’aide d’un Mutex
.
Solution
Vous devez tout d’abord déclarer une instance de Mutex
comme
attribut de la classe:
Mutex mutex_;
Vous devez ensuite protéger la section critique dans la méthode
getAndPrintDateTime
mutex_.lock();
dt.day = currentTime_.day;
dt.hour = currentTime_.hour;
dt.minute = currentTime_.minute;
dt.second = currentTime_.second;
mutex_.unlock();
Vous devez faire de même dans la méthode updateCurrentTime
en
protégeant les accès à currentTime_
.
Réalisation d’une queue à l’aide de sémaphores
Exercice 10
Comme expliqué, la réalisation d’une Queue nécessite également de contrôler le mécanisme de consommation et non seulement de production. Est-ce que la réalisation ci-dessous
class Queue {
public:
...
void put(int datum)
{
inSemaphore_.acquire();
// insert element in buffer
}
int get(void)
{
// pick element from buffer
inSemaphore_.release();
return datum;
}
private:
...
int buffer_[QUEUE_SIZE] = {0};
...
Semaphore inSemaphore_ {QUEUE_SIZE};
};
inSemaphore_
peut être utilisée dans ce but ? Si non,
quel mécanisme faut-il mettre en place ?
Solution
Il est nécessaire de mettre en place un deuxième sémaphore qui contrôle le flux de sortie de la Queue, selon la solution ci-dessous.
class Queue {
public:
...
void put(int datum)
{
inSemaphore_.acquire();
// insert element in buffer
outSemaphore_.release();
}
int get(void)
{
outSemaphore_.acquire();
// pick element from buffer
inSemaphore_.release();
return datum;
}
private:
...
int buffer_[QUEUE_SIZE] = {0};
...
Semaphore inSemaphore_ {QUEUE_SIZE};
Semaphore outSemaphore_ {0};
};
Mutex réentrant
Exercice 11
class A
{
void lock()
{
mutex_.lock();
}
void unlock()
{
mutex_.unlock();
}
private:
//
Mutex mutex_;
}
class B
{
void method1()
{
a_.lock();
...
method2();
...
a_.unlock();
}
void method2()
{
a_.lock();
...
a_.unlock();
}
private:
// owns an instance of A
A a_;
};
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.
Synchronisation avec moniteur
Vous devez synchroniser à l’aide d’un moniteur (ConditionVariable
), une
tâche qui met à jour une donnée dans un objet partagé avec une tâche qui
reprend cette donnée mise à jour pour l’afficher. Il s’agit d’un problème
classique de producteur/consommateur.
Exercice Exercices du chapitre “Modèles de programmation”/12
Créer un objet partagé entre un thread
producer et un thread
consumer. La donnée partagée est mise à jour par la méthode void
setData(int value)
et relue par la méthode int getData()
de l’objet
partagé.
- Le moniteur est caché aux deux
thread
dans l’objet partagé. - La méthode
getData
bloque tant que la donnée n’est pas mise à jour. - La méthode
setData
débloque la méthodegetData
.
Proposez une solution qui fonctionne avec plusieurs consommateurs.
Proposez deux options de solutions où les consommateurs affichent les données soit alternativement, soit tous en simultané à chaque mise à jour.
Solution
#include "mbed.h"
class SharedData {
public:
SharedData() : mutex_(), condition_(mutex_) {}
void setData(int value){
mutex_.lock();
value_ = value;
// condition_.notify_all(); // simultaneously
condition_.notify_one(); // alternatively
mutex_.unlock();
}
int getData(){
mutex_.lock();
condition_.wait();
int value = value_;
mutex_.unlock();
return value;
}
private:
int value_ = 0;
Mutex mutex_;
ConditionVariable condition_;
};
void consumerTask(SharedData* data)
{
while (true) {
printf("%s receives %d \n", ThisThread::get_name(), data->getData());
}
}
int main() // also the producer !
{
SharedData data;
Thread consumeThread1(osPriorityNormal,OS_STACK_SIZE,NULL,"Consumer1");
Thread consumeThread2(osPriorityNormal,OS_STACK_SIZE,NULL,"Consumer2");
consumeThread1.start(callback(consumerTask, &data));
consumeThread2.start(callback(consumerTask, &data));
int value = 0;
while (true) {
printf("Produces %d\n", value);
data.setData(value++);
ThisThread::sleep_for(2s);
}
}