ESP32, FreeRTOS, tâche et classes

Oui, je cherche la difficulté... :confused:

J'ai créé une bibliothèque avec une classe qui fait des calculs un peu longs. Pour aller plus vite, je veux paralléliser, c'est à dire couper mon calcul en deux et faire tourner chaque moitié sur un processeur de l'ESP32.

Donc, je dois utiliser FreeRTOS et créer deux tâches qui vont tourner chacune sur un processeur. Mes tâches ont besoin d'arguments, passés depuis la méthode qui crée ces tâches.

Si je crée mes tâches simplement, j'obtiens une erreur de compilation

error: invalid use of non-static member function

due au fait que je dois informer le compilateur de l'instance qui crée ces tâches.

Il faut donc créer un "wrapper", une tâche qui englobe ma tâche, qui prend en argument l'instance en question. C'est ce qu'on voit ici (stackoverflow).

Mais ce wrapper ne prend en argument que l'instance, du coup je ne sais pas comment passer mes arguments à la tâche interne...

Un peu de code :

Mes arguments sont dans une structure :

    // Create semaphore for tasks synchronization
    SemaphoreHandle_t barrierSemaphore = xSemaphoreCreateCounting( 2, 0 );
    // Arguments for the tasks
    argsStruct params0 = {1, med, l, barrierSemaphore};
    argsStruct params1 = {med + 1, end, l, barrierSemaphore};

La structure contient un sémaphore pour synchroniser mes deux tâches, afin de continuer mes calculs quand elles ont terminé.

Les tâches sont créées ici :

// Create parallel tasks
    xTaskCreatePinnedToCore(
      this->startForwardTask,              /* Function to implement the task */
      "Forward0",               /* Name of the task */
      1000,                     /* Stack size in words */
      this,
  /*    (void*)&params0,          /* Task input parameter */
      20,                       /* Priority of the task */
      NULL,                     /* Task handle */
      0);                       /* Core where the task runs */

    xTaskCreatePinnedToCore(
      this->startForwardTask,             /* Function to implement the task */
      "Forward1",              /* Name of the task */
      1000,                    /* Stack size in words */
      this,
  /*    (void*)&params1,         /* Task input parameter */
      20,                      /* Priority of the task */
      NULL,                    /* Task handle */
      1);                      /* Core where the task runs */

J'ai commenté les passages de mes arguments pour passer les instances à la place (le 'this').

La tâche et le wrapper :

void MLP::startForwardTask(void* _this) {
 (MLP*)_this->forwardTask();
}

void MLP::forwardTask (void * parameters) {
  argsStruct myArgs = *((argsStruct*)parameters);

 // whatever

  SemaphoreHandle_t barrierSemaphore = myArgs.semaphore;
  xSemaphoreGive(barrierSemaphore);
  vTaskDelete(NULL);
}

A la compilation, j'obtiens ceci :

C:---\Arduino\libraries\MLP\MLP.cpp: In static member function 'static void MLP::startForwardTask(void*)':

C:---\Arduino\libraries\MLP\MLP.cpp:887:14: error: 'void*' is not a pointer-to-object type

(MLP*)_this->forwardTask();

^
exit status 1

Donc, j'ai déjà ce problème. Mais ensuite, comment faire pour passer mes arguments (params0 et params1) à la tâche forwardTask ?

(Note : j'ai aussi posé la question sur Stackoverflow, vous la verrez peut-être, mais j'espère une réponse plus rapide ici)

due au fait que je dois informer le compilateur de l'instance qui crée ces tâches.

Je ne vois pas pourquoi.

Il n'y a pas d'intérêt à passer à xTaskCreatePinnedToCore l'adresse d'une méthode de classe. Normalement on lui passe l'adresse d'une simple fonction.
L'argument de la tâche lui est passé au moment de la création. Cela peut être l'adresse d'une structure comme tu veux le faire.
Je ne vois pas l'intérêt de passer par une méthode intermédiaire startForwardTask().
MLP::forwardTask() a déjà le bon prototype.

Je ne vois pas pourquoi tu veux passes this en argument au lieu de params0 et params1.

Tout est fait dans la classe MLP.

Pour le reste, je suis d'accord, je ne vois pas non plus, mais c'est ce qui permet de ne plus avoir d'erreurs de compilation.

Si je fais comme tu dis:

        // Create parallel tasks
        xTaskCreatePinnedToCore(
          forwardTask,              /* Function to implement the task */
          "Forward0",               /* Name of the task */
          1000,                     /* Stack size in words */
       //   this,
          (void*)&params0,          /* Task input parameter */
          20,                       /* Priority of the task 
          NULL,                     /* Task handle */
          0);                       /* Core where the task runs */

        xTaskCreatePinnedToCore(
          forwardTask,             /* Function to implement the task */
          "Forward1",              /* Name of the task */
          1000,                    /* Stack size in words */
      //    this,
          (void*)&params1,         /* Task input parameter */
          20,                      /* Priority of the task */
          NULL,                    /* Task handle */
          1);                      /* Core where the task runs */

j'obtiens cette erreur :

C:---\Arduino\libraries\MLP\MLP.cpp: In member function 'void MLP::propagateNet()':

C:---\Arduino\libraries\MLP\MLP.cpp:942:12: error: invalid use of non-static member function

0); /* Core where the task runs */

^

C:---\Arduino\libraries\MLP\MLP.cpp:952:12: error: invalid use of non-static member function

1); /* Core where the task runs */

^

exit status 1

Et en cherchant cette erreur sur internet, je suis tombé sur ce fil sur stackoverflow.

I use this pattern with a wrapper function for instanciating pthread with non-static member functions. The function called in xTask is a static member function, calling the task function using void* pointer
...

Le problème est qu'il manque pas mal de code.

Mais forwardTask doit être static (méthode de classe).

class MLP
{
  public:
  static void forwardTask (void * parameters);
  // ...

Ensuite :

  xTaskCreatePinnedToCore(MLP::forwardTask, "Forward0", 1024, (void *)&params0, 20, NULL, 0);

C'est bien pour cela que je dis qu'utiliser une fonction serait plus facile, car qu'est ce qui ressemble le plus à une fonction qu'une méthode de classe ?

C'est classique qu'une classe qui crée un thread (je sors du monde Arduino) possède une méthode statique, car le système sous jacent (par ex. pthread_create() pour POSIX) demande de fournir une fonction de démarrage du thread purement C, pas un membre d'une classe.
Le programmeur pourrait écrire une fonction C isolée, mais par souci de propreté, d'encapsulation, il préfère créer une méthode statique dans sa classe

class CThreadServeur {
public:
  CThreadServeur ( int socket );

  // ... 
 
  static void  StaticStart ( void * data ); // data est en fait un CThreadServeur*
         void  Start ();
};

pour lancer le thread, on passe à pthread_start la méthode statique - qui est une pure fonction C, ainsi qu'un pointeur sur l'instance de notre objet CThreadServeur. Le thread démarre sur notre fonction statique, qui fait :

void CThreadServeur::
StaticStart ( void *obj )
{
  CThreadServeur *th = (CThreadServeur*)obj;
  th->Start ();
}

Et nous voilà à l'intérieur de notre proptre classe, on peut accéder aux membres, etc... Cool.

Ce que décrit @lesept ressemble très fort à ça ... sauf qu'il manque effectivement le paramètre data qui permet de revenir "dans la classe". Difficile de dire sans plonger dans la doc de la lib utilisée.

Je ne dis pas le contraire mais le fait de passer this en tant qu'argument au thread implique que les paramètres de fonctionnement du thread soient aussi des membres de l'objet.

voici un petit exemple tapé un peu rapidement et pas testé bcp mais qui devrait fonctionner

//-------- DEFINITION DE LA CLASSE -------
class TraitementParallele
{
  private:
    uint8_t nbTasks;

    struct t_param {
      char taskName[20];
      uint32_t attente;
      SemaphoreHandle_t * semaphore;
    };

    static void leCalcul(void * parameter )
    {
      char message[50];
      for ( int i = 0; i < 5; i++ ) {
        delay(((t_param *) parameter)->attente);
        sprintf(message, "%s\t(#%d/5)\n", ((t_param *) parameter)->taskName, i + 1);
        Serial.print(message);
      }
      Serial.print("End of ");
      Serial.println(((t_param *) parameter)->taskName);
      xSemaphoreGive(*(((t_param *) parameter)->semaphore));
      vTaskDelete(NULL);
    }

  public:
    TraitementParallele(uint8_t n) : nbTasks(n) {}       // constructor

    void computeSomething() {
      SemaphoreHandle_t cntSemaphore = xSemaphoreCreateCounting( nbTasks, 0 );  /* Create a counting semaphore that has a maximum count of nbTasks and an initial count of 0. */

      if ( cntSemaphore != NULL ) {    /* The semaphore was created successfully. */
        t_param taskParams[nbTasks];

        Serial.println(F("CREATING TASKS"));

        for (int i = 0; i < nbTasks; i++) {
          // build specific parameters for each task
          sprintf(taskParams[i].taskName, "Task #%d", i);
          taskParams[i].attente = 250ul * (i + 1);
          taskParams[i].semaphore = &cntSemaphore;
          Serial.printf("\t%s\n", taskParams[i].taskName);

          // launch the task with its parameters
          xTaskCreate(
            TraitementParallele::leCalcul,  /* Task function. */
            taskParams[i].taskName,         /* name of task. */
            1000,                           /* Stack size in bytes. */
            (void *) (&(taskParams[i])),    /* Parameter passed as input of the task. Needs to be a (void *) */
            10,                             /* Priority. Low priority numbers denote low priority tasks. Priority from 0 to (configMAX_PRIORITIES – 1 ), (configMAX_PRIORITIES is defined  in FreeRTOSConfig.h).https://github.com/espressif/esp-idf/blob/0b8f17e6183c401ea6322be384da8a32a75fc76f/components/freertos/xtensa/include/freertos/FreeRTOSConfig.h#L178*/
            NULL);                          /* Task handle. */
        }
        Serial.println(F("\n\nTASKS CREATED"));

        // WAIT FOR TASKS TO COMPLETE
        for (byte i = 0; i < nbTasks; i++) xSemaphoreTake(cntSemaphore, portMAX_DELAY);
        vSemaphoreDelete(cntSemaphore);
        Serial.println(F("ALL TASKS DONE"));
      }
    }
};

//--------

TraitementParallele monInstance(3); // 3 tasks will be asked



void setup() {
  Serial.begin(115200);

  Serial.println(F("Launching computing"));
  monInstance.computeSomething();
  Serial.println(F("Computing done"));
}

void loop() {
  yield();
}

la classe s’appelle TraitementParallele. quand on crée une instance on lui dit combien de tasks on voudra. la méthode computeSomething() se charge d’établir un contexte pour chaque tâche (dans la structure privée) avec un sémaphore partagé de type compteur (Comme la fonction ne finit pas avant la fin des tâches, la mémoire allouée aux contexte est bien toujours présente pendant toute l’exécution de la tâche).

le calcul parallèle à proprement parler ne fait rien si ce n’est imprimer 5 fois un message calculé dynamiquement avec le nom de la tâche et son itération. Chaque tâche a un délai variable entre 2 impressions qui est proposé dans son contexte.

je ne sais pas si ça aide…

Merci à vous, je vais voir ça à tête reposée... (et avec une bonne aspirine !)

Sincèrement si la fonction de ton thread était une simple fonction C, sans chercher à l'encapsuler dans du C++, et en passant simplement l'adresse de ta structure de paramètres à xTaskCreatePinnedToCore, l'aspirine serait inutile.

J'entends bien mais je ne comprends pas. Où est le C++ dans ma fonction ? Dit autrement, je ne connais pas suffisamment les subtiles différences qui font qu'un code est du C ou du C++.

J'ai l'impression que c'est du C...

Je pense avoir essayé ce que tu suggères et le compilateur n'a pas apprécié. Faut-il que je sorte ma task de la classe?

Vous avez jeté un oeil à mon exemple ?

Pas encore, je vais essayer aujourd'hui. Merci J-M-L

Dit autrement, je ne connais pas suffisamment les subtiles différences qui font qu'un code est du C ou du C++.

C'est bien pour cela que je dis que l'on peut créer des threads en pur C.
Sépare les problématiques, commence par coder des tâches en C, ensuite tu pourras envisager le C++.

J'ai l'impression que c'est du C...

void MLP::forwardTask (void * parameters) {

C'est du C++. Le problème est que la fonction principale d'une tâche doit être une fonction C globale ou statique ou une méthode C++ statique.

En partant de l'exemple C : FreeRTOS.ino on voit bien que les fonctions sont écrites en pur C. Il suffit d'ajouter les arguments.

D'autre part je ne pense pas que tu aies besoin de créer plusieurs tâches.
LoopTask() en est déjà une :

   xTaskCreateUniversal(loopTask, "loopTask", 8192, NULL, 1, &loopTaskHandle, CONFIG_ARDUINO_RUNNING_CORE);

La loopTask est créée avec une priorité de 1 sur le core 1, il te suffit d'en créer une autre sur le core 0 avec la même priorité.

Ensuite tu n'as plus qu'à gérer l'aspect synchro.

Oui mais comme Une méthode de classe statique se comportera comme une fonction en C pour ce besoin avec le petit plus que c’est caché (private) dans la classe s’il veut faire un truc encapsulé avec une API simple pour l’utilisateur à la mode arduino. (Certes on peut toujours faire à peu près pareil en C )

En fait ce sera transparent (invisible) pour l'utilisateur.

Je pense faire deux tâches pour séparer les calculs : par exemple, boucles de 1 à 100 sur proc 0 et boucle de 101 à 200 sur proc 1. Facteur d'accélération proche de 2 (vérifié sur des exemples simples).
Ensuite, lorsque les deux ont terminé, le programme actuel reprend la main et continue.

oui on a bien compris.

Vous pouvez soit planquer la méthode dans la classe comme je le fais dans mon exemple en la déclarant statique, soit simplement planquer une fonction dans le .cpp sans la déclarer dans le .h pour que l’utilisateur ne la voit pas.

j’ai modifié mon exemple pour vous montrer comment parler à votre instance dans le cadre de l’exécution de la tâche en embarquant this dans les paramètres.

J’utilise l’instance qui a forké les tâches quand j’appelle ((t_param *) parameter)->instance->howManyTasks()La première partie vise à redonner un type à la structure puisque le pointeur passé est un void * afin que le compilo sache ce qu’il y a dedans et ensuite je vais chercher la variable instance de cette structure que j’avais pris soin d’initialiser à this avant la création de la tâche en faisanttaskParams[i].instance = this;et puisque cette variable pointe sur un objet de type TraitementParallele, je peux appeler la méthode howManyTasks() et donc d’afficher à l’écran le nombre de tâches demandées à l’instance.

//-------- DEFINITION DE LA CLASSE -------
class TraitementParallele
{
  public:
    TraitementParallele(uint8_t n) : nbTasks(n) {}       // constructor

    int howManyTasks() {
      return nbTasks;
    }

    void computeSomething() {
      SemaphoreHandle_t cntSemaphore = xSemaphoreCreateCounting( nbTasks, 0 );  /* Create a counting semaphore that has a maximum count of nbTasks and an initial count of 0. */

      if ( cntSemaphore != NULL ) {    /* The semaphore was created successfully. */
        t_param taskParams[nbTasks];

        Serial.println(F("CREATING TASKS"));

        for (int i = 0; i < nbTasks; i++) {
          // build specific parameters for each task
          sprintf(taskParams[i].taskName, "Task #%d", i);
          taskParams[i].attente = 250ul * (i + 1);
          taskParams[i].semaphore = &cntSemaphore;
          taskParams[i].instance = this;
          Serial.printf("\t%s\n", taskParams[i].taskName);

          // launch the task with its parameters
          xTaskCreate(
            TraitementParallele::leCalcul,  /* Task function. */
            taskParams[i].taskName,         /* name of task. */
            10000,                           /* Stack size in bytes. */
            (void *) (&(taskParams[i])),    /* Parameter passed as input of the task. Needs to be a (void *) */
            10,                             /* Priority. Low priority numbers denote low priority tasks. Priority from 0 to (configMAX_PRIORITIES - 1 ), (configMAX_PRIORITIES is defined  in FreeRTOSConfig.h).https://github.com/espressif/esp-idf/blob/0b8f17e6183c401ea6322be384da8a32a75fc76f/components/freertos/xtensa/include/freertos/FreeRTOSConfig.h#L178*/
            NULL);                          /* Task handle. */
        }
        Serial.println(F("\n\nTASKS CREATED"));

        // WAIT FOR TASKS TO COMPLETE
        for (byte i = 0; i < nbTasks; i++) xSemaphoreTake(cntSemaphore, portMAX_DELAY); // decrementing the semaphore count value
        vSemaphoreDelete(cntSemaphore);
        Serial.println(F("ALL TASKS DONE"));
      }
    }

  private:
    int nbTasks;

    struct t_param {
      char taskName[20];
      uint32_t attente;
      TraitementParallele* instance;
      SemaphoreHandle_t * semaphore;
    };

    static void leCalcul(void * parameter )
    {
      char message[50];
      for ( int i = 0; i < 5; i++ ) {
        delay(((t_param *) parameter)->attente);
        sprintf(message, "%s/%d\t(#%d/5)\n", ((t_param *) parameter)->taskName, ((t_param *) parameter)->instance->howManyTasks(), i + 1);
        Serial.print(message);
      }
      Serial.print("End of ");
      Serial.println(((t_param *) parameter)->taskName);
      xSemaphoreGive(*(((t_param *) parameter)->semaphore));  // incrementing the semaphore count value
      vTaskDelete(NULL);
    }

};

//--------

TraitementParallele monInstance(3); // 3 tasks will be asked



void setup() {
  Serial.begin(115200);

  Serial.println(F("Launching computing"));
  monInstance.computeSomething();
  Serial.println(F("Computing done"));
}

void loop() {
  yield();
}

le moniteur série (@ 115200 bauds) affichera

[color=purple]
Task #0/3	(#1/5)
Task #0/3	(#2/5)
Task #1/3	(#1/5)
Task #0/3	(#3/5)
Task #2/3	(#1/5)
Task #0/3	(#4/5)
Task #1/3	(#2/5)
Task #0/3	(#5/5)
End of Task #0
Task #1/3	(#3/5)
Task #2/3	(#2/5)
Task #1/3	(#4/5)
Task #2/3	(#3/5)
Task #1/3	(#5/5)
End of Task #1
Task #2/3	(#4/5)
Task #2/3	(#5/5)
End of Task #2
ALL TASKS DONE
Computing done

[/color]

Merci J-M-L, ça me parait clair. Je vais l'appliquer à mon cas.

J'aurais été bien en peine de trouver tout ça !!!

J’ai essayé la première proposition : lorsque je la teste telle quelle, le code marche bien.

Lorsque je veux l’appliquer à mon cas, j’ai une erreur de compilation (plusieurs fois la même) :

In file included from C:—\Documents\Arduino\MLP\MLP_Sinus\MLP_sinus.ino:5:0:

C:—\Documents\Arduino\libraries\MLP/MLP.h: In static member function ‘static void MLP::forwardTask(void*)’:

C:—\Documents\Arduino\libraries\MLP/MLP.h:320:26: error: invalid use of member ‘MLP::Layer’ in static member function

for (int j = 0; j <= Layer[l]->Units; j++) {

^

C:—\Documents\Arduino\libraries\MLP/MLP.h:332:14: note: declared here

LAYER** Layer; /* - layers of this net */

Je teste la seconde méthode de J-M-L, qui embarque l’instance dans les paramètres et cette erreur disparaît, mais il en arrive une nouvelle, moins bavarde :

In file included from C:—\Documents\Arduino\MLP\MLP_Sinus\MLP_sinus.ino:5:0:

C:—\Documents\Arduino\libraries\MLP/MLP.h: In static member function ‘static void MLP::forwardTask(void*)’:

C:—\Documents\Arduino\libraries\MLP/MLP.h:328:40: error: cannot call member function ‘float MLP::activation(float, LAYER*)’ without object

LP->Output = activation (Sum, LP);
[/quote]
Il semble que je ne puisse pas appeler une méthode publique de ma bibliothèque aussi simplement que ça : quelle syntaxe utiliser pour résoudre ce problème ?
Un extrait du code (trop long pour le forum) :
```

  • private:

struct argsStruct { // structure to pass arguments to parallel tasks
 int start;
 int end;
 int layer;
 MLP * instance;
 SemaphoreHandle_t * semaphore;
};

static void forwardTask (void * parameters) {
 argsStruct myArgs = ((argsStruct)parameters);

int product;
 int start = ((argsStruct *) parameters)->start;
 int end   = ((argsStruct *) parameters)->end;
 Serial.print(pcTaskGetTaskName(NULL));
 Serial.print("\tdebut “);
 Serial.print(start);
 Serial.print(”\tfin ");
 Serial.println(end);

float Sum;
 int l = myArgs.layer;
 LAYER* L  = ((argsStruct ) parameters)->instance->Layer[l];
 LAYER
LP = ((argsStruct *) parameters)->instance->Layer[l + 1];
 for (int i = myArgs.start; i <= myArgs.end; i++) {
   Sum = 0;
   for (int j = 0; j <= L->Units; j++) {
     Sum += LP->Weight[i][j] * L->Output[j];
   }
   LP->Output[i] = activation (Sum, LP);
 }

Serial.print ("end of task ");
 Serial.println (start == 1?“0”:“1”);
 SemaphoreHandle_t barrierSemaphore = ((argsStruct *) parameters)->semaphore;
 xSemaphoreGive(barrierSemaphore);
 vTaskDelete(NULL);
}

// Parameters of the network
   LAYER**  Layer;         /* - layers of this net           /
   LAYER
  InputLayer;    /* - input layer                  /
   LAYER
  OutputLayer;   /* - output layer                 /

* *EDIT : EUREKA !* *J'ai ajouté l'instance devant l'appel de fonction et ça compile !!!* *

  • LP->Output[i] = ((argsStruct ) parameters)->instance->activation (Sum, LP);
    ```

Losque tu es dans une fonction-membre (non statique) d’une classe, il y a un paramètre invisible qui est passé à la fonction, c’est le this, qui désigne l’instance de la classe, l’objet qui travaille.
Par simplification, on n’ecrrit pas le this explicitement

MaClasse::MaFonctionMembre () {
  this->membre1 = 0; // écriture complète
  membre1 = 0; // écriture simplifiée.
}

Dans une méthode statique, il n’y a pas de this. Cette méthode n’est pas appelée pour un objet particulier, c’est comme une fonction C (sauf qu’en tant que membre, elle a accès aux autres membre privés de sa classe, ça peut servir).
Quand tu écris

     for (int j = 0; j <= Layer[l]->Units; j++) {

Layer est un membre de la classe. Donc il faut un objet existant pour y accéder.
Et dans méthode statique, il n’y en a pas → nicht compiliert !

C’est entre autre pour ça que la manière POSIX pour lancer un thread (cf mon post #'4) te permet de passer un pointeur qu’on peut utiliser pour passer le this manquant.

EDIT : EUREKA !

Oui et l'explication de @biggil est correcte