Publié le 31 juillet 2016 - par

Penser ‘Thread’ pour simplifier vos programmes

bs01_threadssDans les exemples du premier article de La Saga Blink, l’auteur mettait en avant l’intérêt de pouvoir se sortir de la boucle de clignotement d’une LED en proposant une ‘combine’ non bloquante de contrôle des caractères. Une solution qui peut paraître satisfaisante, mais l’est-elle réellement ? …

Bien que parfaitement fonctionnels, de nombreux exemples proposés souffrent d’une même anomalie que l’on ne peut ignorer du fait qu’elle remet en cause l’usage même de ces exemples dans d’autres programmes. Voyons donc pourquoi et comment faire autrement.

Exemple de surconsommation critique de ressource.

Pour l’exemple, j’utiliserai le cas du code Blink81. Pourquoi celui-là ? Déjà parce qu’il en faut un de concret. Ensuite parce que ce code est écrit en C sans GUI et utilise la librairie WinringPi. Parmi tous les exemples de l’article cité, c’est théoriquement celui qui devait s’avérer être le plus ‘économique’ et performant. Ceci dit, vous trouverez exactement la même anomalie dans la plupart des autres exemples similaires du même article, qu’ils soient écrit en C ou en Python.

J’ai donc téléchargé les sources et j’ai procédé à la compilation, conformément aux recommandations de l’auteur. Le programme compile sans problème et fait exactement ce qu’on attend de lui, à savoir, ‘blinker‘ une LED à une fréquence de ~1Hz.

Vu que cela fonctionne, Intéressons-nous maintenant à l’incidence de ce programme sur le système en utilisant simplement la commande ‘top’. Celle-ci indique en entête les ressources globales utilisées et celles disponibles puis la liste des processus les plus gourmands depuis le dernier rafraichissement

bs01_top1

Ce que l’on constate immédiatement, c’est que le processus Blink81 occupe le haut du pavé avec 100% d’usage CPU. On peut admettre que le Pi (ici un Pi3) n’est pas un supercalculateur ni même un ordinateur de bureau, mais une telle dépense  juste pour faire clignoter une LED à une fréquence de 1 Hz, cela indique clairement  problème.

Limiter l’occupation dans les boucles infinies.

Les problèmes, Il ne suffit pas de le remarquer, faut-il encore en identifier les causes et les corriger. Ici, inutile de commenter tout le code, le problème se trouve dans la boucle for( ;;) et plus globalement dans la conception globale du programme lui-même. Si vous regardez cette boucle, elle contrôle les ‘éventuelles’ lettres saisies au clavier. Ensuite elle regarde si ‘éventuellement’ le temps d’état de changement est atteint, auquel cas, elle modifie cet état, puis recommence. Tout ceci ne représente tout au plus que quelques dizaines d’instructions banales, qu’un programme en C est normalement capable de traiter à la vitesse de la lumière sans broncher …

…Et c’est bien là le problème. Ces dizaines d’instructions sont ici exécutées, des milliers et des milliers de fois, pour finalement ne rien faire. En fait, cette boucle tourne aussi vite qu’elle peut jusqu’à consommer l’intégralité des ressources CPU qui lui sont allouées (et ça tourne vite le C, même sur un Pi rikiki …). Ajoutons à cela le fait que la fonction kbhit() mine de façon conséquente le rapport cyclique du signal et fatalement aussi sa fréquence.

Pour ceux qui ont chargé cet exemple je vous fais une suggestion toute simple pour estimer la quantité astronomique d’instructions inutiles qui sont exécutées. Ajoutez juste la ligne usleep(1000); dans la partie systématique de la boucle et constatez par vous-même le résultat avec la commande top.

Voilà ce que j’obtiens chez moi sur le Pi3:

bs01_top2

Édifiant non ? En fait on a juste dit à la boucle de se reposer 1 seule petite milliseconde à chaque tour. Pensez donc à laisser reposer vos boucles, surtout si celles-ci n’incluent pas d’instructions bloquantes

Le problème, c’est que cette petite temporisation n’est forcément pas que bénéfique. La durée de pulsation et le rapport cyclique n’étaient déjà pas terrible à cause du nombre d’instructions exécutées entre chaque changement d’état et forcément ce ‘débogage’ est un peu comme un pansement sur une jambe de bois qui ne fait que masquer le véritable mal. Tant que l’on veut se contenter de ‘blinker’ une LED, ça peut le faire, mais si on a besoin d’un signal un peu plus précis et surtout la possibilité de faire autre chose dans le programme ou encore d’utiliser la méthode dans un autre, ça ne le fera jamais, et pour cause. Si l’appel kbhit() est non bloquant, la boucle globale l’est. Si vous voulez faire autre chose de plus avec votre programme, vous serez obligé de l’inclure dans cette boucle, ce qui aura pour effet d’accentuer le problème.

Un exemple de thread simple pour se libérer des boucles dans les boucles.

Comme l’idée n’était pas de critiquer bêtement une méthode mais bien de proposer une alternative qui puisse répondre plus favorablement aux cas plus exigeants d’un simple blink, je vous soumets ce tout petit programme sans prétention. Il fait exactement la même chose que Blink81 et que ses homologues en Python. Je profite de l’occasion pour ajouter une librairie qui permet d’accéder aux GPIO et qui n’avait pas été utilisée dans le tuto précédent. Il s’agit de la librairie bcm2835. On ne voit pas trop de gens l’utiliser sur les forums. Je pense que c’est parce qu’elle ne semble pas aussi ‘avenante’ que la célèbre WiringPi. Il faut la compiler pour l’installer ce qui la rend par conséquent moins populaire et c’est bien dommage, parce qu’à ma connaissance, c’est de très loin la plus performante. Pour la programmation, j’ai utilisé aussi un langage différent, à savoir le C++. Une fois la lib2835 installée, La compilation se fait donc par la ligne suivante:
g++ -o monblink main.cpp –Wall –lpthread –lbcm2835
Notez le g++ au lieu de gcc utilisé pour Blink81. Pour le coté hardware, c’est la même GPIO que pour Blink81 (la broche 16 du connecteur, GPIO23). Maintenant, compilez, exécutez (pas besoin de sudo pour ca avec cette lib), et comparez.

bs01_blinkbcm2835

Vous pouvez chercher monblink dans le ‘top’ mais vous avez peu de chance de le voir dans la liste et s’il advenait qu’il y apparaisse, ce serait très ponctuellement et avec une valeur de consommation CPU infime. Pourquoi ? Hé bien tout simplement parce que ce programme ne fait rien. Enfin, disons qu’il ne fait surtout rien d’inutile. Si on veut apercevoir le process dans top, on peut essayer. Il suffit par exemple de passer la valeur DELAIS à 1 au lieu de 500. Malgré cela, le procès consommera toujours bien moins que l’exemple Blink81 ‘débogué’ et pourtant avec une vitesse de blink 500 fois plus rapide.

La subtilité de ce programme tient dans une seule notion, celle des threads. Il faut considérer les threads un peu comme des portions de programmes qui tournent à l’intérieur du programme principal. Pour blinker une LED, il n’y a pas besoin de 50 instructions qui s’exécutent des milliers de fois pour ne rien faire. Fondamentalement 2 ou 3 suffisent pour peu qu’elles soient utilisées exclusivement quand il y en a vraiment besoin.

J’ai la conviction qu’un code aussi simple est suffisamment explicite pour se passer de commentaire mais je vais quand apporter quelques précisions.

La procédure ‘blinkLed()
– La variable booléenne b représente la valeur ‘on’ ou ‘off’ que l’on va utiliser pour initialiser l’état de la led.
– En début de boucle, bcm2835_gpio_write écrit la valeur b sur la pin gpio LED
– La ligne bcm2835_delay met la boucle en pause pour temps DELAIS (milliseconde).
– Au réveil, la valeur de b est inversée et la boucle retourne à son début.

Le ‘main()’
bcm2835_init initialise la librairie et en cas d’échec le programme s’arrête en retournant une erreur.
bcm2835_gpio_fsel initialise la pin LED en sortie
– Ensuite pthread_create. C’est la toute la subtilité du programme. Cette ligne va stocker la procédure blinkLed() dans un espace réservé et cette procédure va s’exécuter. blinkLed() est désormais un thread secondaire qui s’exécute en arrière-plan du thread principal (main()) qui lui peu continuer sa route.
– La boucle while(s != q)… attend qu’une chaine de caractères soit validée au clavier. Si cette chaine est ‘q’, le programme poursuit sa route, sinon il attend.
pthread_cancel, comme son nom l’indique donne l’ordre d’arrêter le thread secondaire (donc la procédure blinkLed()) et la ligne pthread_join attend que cet arrêt soit effectif avant de poursuivre.
– Comme on ne sait pas à quel moment de son exécution le thread s’est arrêté, on force l’extinction de la LED puis on libère les ressources occupées par bcm_2835.

On aurait très bien pu utiliser une variable pour conditionner la sortie de la boucle de blinkLed et ainsi se passer de pthread_cancel, mais ici cela n’aurait fait que compliquer l’exemple et ne nous aurait rien apporté de plus. Par contre cela aurait été indispensable notamment si il y avait eu d’autres instructions a exécuter dans blinkLed() après la sortie de la boucle. En effet, pthread_cancel arrête le thread a l’endroit ou il se trouve, ce qui n’est pas systématiquement une bonne chose.

Pour ceux qui désirent ‘tester’ sans avoir à installer la libraire bcm2835, rien de plus simple. Voici une variante qui utilise cette fois ci la libraire WiringPi et a compiler comme suit :
g++ -o monblink main.cpp –Wall –lpthread –lwiringPi

bs01_blinkwiringPi

Les threads dans les différents langages.

Si vous regardez ce code, vous pouvez être amené à penser qu’il n’y a pas beaucoup de différence entre C et C++. Dans ce cas de figure, effectivement et c’est parfaitement volontaire. En modifiant uniquement quelques noms de fonction, ce programme pourrait tout à fait être compilé en C. Mais ne vous y tromper pas, en réalité, ces 2 langages sont très différents l’un de l’autre.
Concernant les développeurs Python, sachez que le langage possède aussi une notion de thread. A mon sens, c’est un des langages parmi les plus pauvres dans ce domaine, mais ils permettent déjà beaucoup et cela vaut vraiment la peine de s’y intéresser. Sinon, parmi les langages managés disponibles facilement sur le Pi, Java et C# sont très performants pour gérer du multithread. Connaissant bien ces 2 langages sur les plateformes win, je dirais qu’à aujourd’hui, C# est de très loin le plus évolué pour ça, mais n’ayant pas suffisamment de recul avec le portage Mono sous linux, je ne pourrais pas l’affirmer pour le Pi (et ce n’est qu’un avis personnel).

Conclusion.

J’avais été surpris de voir des commentaires quasi unanimes qui pensaient que les threads étaient forcément trop compliqué voir même ‘hors sujet’ pour faire simplement clignoter une LED. Pour les débutants, il faut faire simple même si c’est au détriment d’un peu de performance me disait-on. Sur ce dernier point je suis tout à fait d’accord et je vous laisse juger de la simplicité de cet exemple ‘threadé’ comparé aux ‘usines à gaz’ cités précédemment. A mon sens, le clignotement d’une LED dans un programme justifie pleinement l’usage d’un thread et je dirais même que c’est le type de cas qui démontrera en premier que c’est quasi indispensable.

J’espère que ce petit article aura contribué à chasser le méchant fantôme des threads. En tout cas, il aura démontré que l’on peut très bien faire des programmes lourds, compliqués et même bogués si on à trop peur de les utiliser. Après, il faut être conscient que le multithreading est une notion extrêmement puissante au sein d’un programme. Il ne suffit pas de démarrer des tas de thread dans tous les sens, il faut aussi les contrôler et bien réfléchir à l’effet de chacun sur les autres. Il existe des tas de choses pour s’accommoder des threads dans tous les cas d’usage. Notamment les notions de verrou et de de priorité, mais là, je vous l’accorde, cela dépasse le cadre du cas d’usage d’un simple ‘blinking’ qui n’a pas besoin de tout ça.

Références.

La librairies BCM2835
Un peu d’aide en Français.
La commande top sous linux
Autres exemples pthread

Share Button

16 réflexions au sujet de « Penser ‘Thread’ pour simplifier vos programmes »

  1. Ping : Penser ‘Thread’ pour simplifier vos programmes

  2. icare

    Bonjour,
    J’ai lu avec beaucoup d’attention ton topo sur les threads qui est fort intéressant par ailleurs.
    Je suis satisfait que l’un de mes exemples ai permis la rédaction de cet article.
    Pour ma part, la vision était plutôt cartes embarquées (Arduino et consoeurs) donc dédiées process.

    Le partage de ressources est une chose importante mais tout est une question de compromis et de choix.

    Je réalise des montages de lévitation magnétique et des gyropodes depuis plus de 10 ans. Dans ces cas, on ne se pose pas trop la question où est la ressource. 😉
    Si l’exploitation des entrées / sorties GPIO avec le monde extérieurs échange également avec d’autres applicatifs de la framboise, il est normal de partager les ressources.
    Pour un fin de course d’un dispositif mécanique c’est autre chose.

    Il existe différentes approches et toutes sont louables.
    @+

    Répondre
  3. Lolo

    Bonjour,
    Comme d’habitude, très bon article !
    Très bonne démonstration de l’utilité des threads et de la notion de consommation de temps CPU.
    Ne pas oublier aussi d’autres approches semblables, comme le timer, qui va périodiquement exécuter une fonction (de nombreux langages et librairies permettent d’utiliser un timer).
    L’avantage par rapport à un thread contenant un sleep, c’est que l’erreur de précision dans le temps ne se cumulera pas (lorsque ça a une importance) et qu’on a pas un thread qui tourne continuellement pour attendre le plus clair de son temps.

    Répondre
    1. Bud Spencer

      Très bonne observation Lolo et tu as tout à fait raison. On pourrait être effectivement bien plus stable sur la durée en utilisant une base de temps externe et indépendante de la fonction. Et si on voulait aller encore plus loin, on pourrait profiter des avantages des 2 méthodes pour faire en sorte que cette fonction n’ait pas d’incidence sur le déroulement du reste du programme en la plaçant dans un thread qui serait abonné à l’évent du timer.

      Répondre
    2. Geo

      effectivement, je te rejoinds Lolo, et te complète :
      après les méthodes, procédures, fonctions… l’une des premières notions qui suis en programmation, est les interruptions… et parmis celles-ci >> les timers (temporisation interruptive)

      principal avantage, cela permet des actions périodique d’une régularité parfaite !
      explication :
      1) imposer un delay (temporisation) de 1seconde puis effectuer des instructions de quelques mili-secondes et faire tourner ça en boucle est correct… mais plus la période sera courte, et plus le temps du cycle sera faussé.
      2) régler un timer sur une durée de 1 seconde pour y effectuer des instructions de quelques mili-secondes et faire tourner ça en boucle, c’est mieux !!… peux importe le temps que prend cette série d’instruction, exactement 1seconde après le début de son exécution, elle s’effectura à nouveau.

      Car le timer ne fait que déclencher un système sous-jacent, il ne fait que compter et déclencher des interruptions… c’est une partie autonome qui ne tient compte ni des thread, ni de l’occupation CPU, ni d’un quelconque mode veille… il d’éclenche une interruption ou il ne déclenche pas, c’est tout. Le seul prérequis est de paramètrer les réglages pour permetre l’interruption dans les conditions souhaités.

      Répondre
  4. j2c

    Super article constructif, j’ai encore appris quelque chose 🙂

    Je pensais en effet que les threads étaient plus compliqués à réaliser.

    Je m’inspirerai de cet exemple pour faire du pilotage de circulateurs en « PWM basse fréquence »… Il me restera à trouver comment faire communiquer mes threads entre eux (actuellement tout ce qui est dans le thread est local au thread, et on ne peut pas simplement faire varier la période ou le rapport cyclique depuis le programme principal.. un bon exercice 🙂 )

    Ayant suivi les « effusions » sur l’autre article, je me désolais de ne pas avoir la suite de l’histoire avec un exemple concret, merci d’avoir pris du temps pour nous exposer une solution plus élégante 🙂

    Répondre
    1. Bud 'Robert' Spencer Auteur de l’article

      Tout ça n’est que question de portée de tes variables.
      Démonstration (sur la base de la variante wiringPi de l’article).

      Surprime la ligne #define DELAIS 500
      Juste avant la void blinLed, ajoutes les lignes suivantes :
      static unsigned int DELAIS;
      static bool run;

      Dans la void blinkLed
      Remplace while(true) par while(run)
      Ajoute la ligne return(0); juste après l’accolade de fin de boucle.

      Dans la void main
      Juste après la déclaration de string s , ajoutes les lignes suivantes :
      run = true;
      DELAIS = 500;
      Remplace la ligne pthread_cancel(t); par la ligne run = false;

      Vue que tu as mis run à true avant de démarrer ton thread, la boucle de blinkLed va ‘boucler’.
      Si tu valides la commande ‘q’, run va passer à false sur la ligne suivante et ton thread va se terminer normalement.
      Comme les variables DELAIS et run sont déclaré avant et à l’extérieur des void, elle sont static sur sur tout le fichier.

      En ajoutant quelques lignes, on peut très facilement transformer tout ca en un pwm que l’on pourrait démarrer/arrêter et changer le rapport cyclique dynamiquement et même la fréquence, juste par des commandes consoles, mais ce n’est guère pratique de poser du code dans les commentaires et si je te dis tout, tu vas t’ennuyer 😉

      Répondre
      1. j2c

        Si j’ai bien compris tu utilises des variables globales pour permettre la communication entre threads.

        Le fait de les déclarer statique, je n’ai pas bien compris la subtilité. Il me semble que ça a surtout un sens pour une variable locale, qui conserve sa valeur entre 2 passages dans une fonction (ou procédure). Je me trompe ?

        Merci pour cette piste à creuser 🙂

        Là, je suis pas en manque de taf ou d’idée.. mais en manque de temps pour les réaliser 🙂

        Répondre
        1. Bud 'Robert' Spencer

          C’est tout à fait ça. A une nuance près, c’est qu’une variable static n’est pas une variable globale, mais ici, vu que l’on a qu’un seul fichier, on peut effectivement considérer que les ‘static’ sont globales. Avec ce type de programmation, une static porte uniquement sur le fichier de déclaration alors qu’une variable globale porte sur l’ensemble du programme. En poo c’est un peu diffèrent, une static porte sur la class elle-même et non pas indépendamment pour chacune de ses instances.

          Répondre
      2. Coussinet

        Attention toutefois, l’utilisation de « simples » variables pour la communication inter-threads est risquée pour ne pas dire complètement viciée. L’ordre des lectures et des écritures n’est pas garanti, ni même la cohérence des données échangées. On n’est même pas sûr que le programme va bien lire régulièrement une variable que le compilateur va juger constante lors de la phase d’optimisation. Et rajouter le mot-clé « volatile » comme on le voit souvent est loin d’être suffisant.

        Heureusement la plupart des langages disposent d’une panoplie de fonctions ou de primitives pour s’envoyer des messages entre threads tout en offrant des garanties de cohérence. Évidemment, c’est souvent plus lourd à mettre en œuvre, mais ça permet d’éviter beaucoup de mauvaises surprises.

        Répondre
  5. franck

    Bonjour,

    Merci pour ces articles, qui pour une simple problématique permet de faire le tour des possibilités de faire clignoter une LED. En continuant sur cette lancée, serait-il possible d’avoir un exemple sur la mise en service (daemon), histoire de faire clignoter notre LED au démarrage du PI. J’ai vu qu’il fallait passer par un fork() de notre application mais ça reste un peu floue pour moi.

    Pour compléter ce kamasutra de la LED clignotante il y a aussi la possibilité de créer un module kernel proposé par Guillaume BLAESS à l’adresse suivante : http://www.blaess.fr/christophe/2012/11/26/les-gpio-du-raspberry-pi/

    Répondre
  6. Bernard B

    Article très intéressant et fort bien rédigé par Spencer. Merci pour son travail pédagogique.

    Mais si je peux faire une petite remarque générale, après tout on est au mois d’août, il me semble qu’on sort un peu du cadre de ce blog que François a voulu plus orienté hard avec des articles de découverte du Pi.

    Pas certain que les cours de programmation en C++ et les threads soient forcement bien adapté aux utilisateurs de la Framboise et surtout aux visiteurs de Framboise314.
    La définition d’un utilisateur du PI ? : un homme, ou une femme (oui, oui il y en a aussi ) curieux d’apprendre et de chercher sans cesse comment ça marche et surtout en y prenant du plaisir.

    Personnellement les cours de vacances très peu pour moi. 😉

    Bon, mais ce que j’en dis ! Cordialement

    PS: François, j’espère que tu ne prendra pas ta retraite du blog ?

    Répondre
  7. Terence Hill

    Je ne peux que dire Bravo, ton article à lui seule exprime bien mieux ce que tu voulais dire dans l’article de la LED. Pour les plus perfectionnistes ou les plus curieux associer les 2 articles sera une bonne chose.

    Pour ma part je considère cela comme des TP c’est à dire que je ne me soucis pas dans l’article précédent de la consommation de ressources (c’est peut être un tort je l’avoue) car au bout d’une heure ce sera démonté et j’aurais mis autre chose dessus. Du coup je ferais 2 TP 😉

    J’apprécie que tu ais fait cet effort et surtout la façon dont c’est rédigé car la première fois que j’ai eu un cours sur le sujet -> ok ca remonte à loin, le PROF avait été tellement pas pédagogue qu’on est tous ressortis de là avec une tête comme une pastèque, bon on a vite compris que ce n’était pas le sujet qui était difficile mais la façon de le présenter. c’est comme tout quand on y met les formes ou les bons mots. Par contre il avait tendance à mettre des thread partout je me demande même à quel point il ne nous considérait pas comme tel 😉

    PS: Je comptais tester un peu tout ca mais je n’ai pas eu le temps et il va me falloir une(des) autre(s) framboise si ca continu

    A+ TOUS

    Répondre
  8. Ping : La Saga Blink Suite : Elle clignote toujours | Framboise 314, le Raspberry Pi à la sauce française….

  9. Eric

    Merci pour ce super tuto !

    Une question : j’ai 3 threads, si je change le délai pour l’un d’entre eux ma led saccade a l’allumage / extinction. Je ne comprend pas pourquoi …

    Une autre au passage, il m’est impossible de faire du PWM

    Répondre
  10. Eric

    Bon … je n’ai pas fait de modifs mais seulement ajouté 2 threads supplémentaires.
    Tout fonctionne bien, y compris le PWM (je ne me souvenais plus que seule la PIN physique 12 pouvait le faire)

    Encore merci pour le tuto !

    Répondre

Laisser un commentaire

Votre adresse e-mail ne sera pas publiée.

Complétez ce captcha SVP *

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.