Aller au contenu

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

Tâche
Période avec les spécifications temporelles

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);
}
Dans ce code, la fonction 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
superloop_two_leds.cpp (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?

button_polling.cpp
#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
button_interrupt.cpp (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 de EventQueue clockDisplayQueue_. Supposons que les deux premières instructions C++ soient exécutées et que les valeurs {0, 10} soient donc copiées dans dt.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 variable dt (copiant donc {0, 0} dans les champs dt.minute/second). La valeur des champs de la variable dt 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

queue_in.hpp
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};
};
garantit le fait qu’un consommateur ne pourra pas consommer de données si aucune donnée n’est disponible dans la Queue ? Si non, est-ce que le sémaphore 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.

queue_inout.hpp (solution)
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

mutex_composition.hpp
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_;
};
Dans le code 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.

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éthode getData.

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
monitor_producer_consumer.cpp (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);
    }
}