SOCIÉTÉ

BLOGS

Espèce de sémaphore !

19.01.2010
par Olivier Deschanels

Si j'emprunte aujourd'hui cette injure du capitaine Chester à l'encontre du capitaine Haddock, c'est pour moi aussi jurer à la vue de l'utilisation de la commande sémaphore de 4D. J'ai trop souvent vu des bases qui soit ignorent complètement cette commande, soit l'utilisent de manière telle que notre cher capitaine Haddock qualifierait les auteurs du code de nyctalopes.

Je vais donc me permettre d'allumer un phare pour éclairer l'usage correct d'un sémaphore.

 

En informatique un sémaphore est un outil très pratique permettant de protéger des actions qui ne doivent être réalisées que par un seul processus à la foi.
Dans le monde 4D, l'exemple classique d'usage d'un sémaphore est la modification d'un tableau interprocess.
Un développeur utilise un sémaphore pour indiquer à un processus qu'il ne peut réaliser la suite des opérations que si aucun autre processus n'est déjà en train de réaliser les mêmes tâches. Concrètement, lorsqu'un processus se heurte à un sémaphore, il y a trois solutions :

  • il obtient le droit de passer
  • il attend son tour, et cela jusqu'à ce qu'il obtienne le droit de passer
  • il passe son chemin en abandonnant l'idée de réaliser les tâches.

Nous voyons bien ici que le sémaphore implique une notion de protection de code. Le sémaphore ne laisse passer qu'un processus à la fois et interdit l'accès tant que le processus détenteur du droit d'usage ne redonne pas son droit en libérant le sémaphore.

Dans le langage 4D, pour poser un sémaphore nous utilisons la fonction Semaphore ; pour libérer un sémaphore, la commande EFFACER SEMAPHORE sera utilisée.
La fonction sémaphore est une fonction au comportement très particulier car elle réalise potentiellement deux actions en même temps :

  • si le sémaphore est déjà attribué, la fonction répond "vrai"
  • si le sémaphore n'est pas attribué, la fonction attribue le sémaphore au processus et répond "faux" dans le même temps.

Il est fondamental que le test du sémaphore et son attribution soient réalisés de manière concomitante et sans possibilité qu'une autre action vienne s'insérer entre les deux actions nécessaires à la pose d'un sémaphore. Pour cela, il n'y a d'autre moyen que de travailler avec la commande 4D qui garantit totalement l'atomicité de l'opération.

Trêve de théorie, passons à la pratique !


Personnellement je n'utilise les sémaphores que d'une seule manière et en respectant le schéma suivant que j'érige en règle d'or :

  • Je pose et je libère le sémaphore toujours dans la même méthode
  • Je cherche à réduire au maximum le code protégé par le sémaphore, ou plus exactement le temps nécessaire à l'exécution de ce code
  • Je temporise toujours mon code pour attendre la libération du sémaphore


Voici le code typique du l'utilisation d'un sémaphore :

 

Tant que (Semaphore("MonSemaphore";500))
    APPELER 4D
Fin tant que

... code protégé par le sémaphore

EFFACER SEMAPHORE("MonSemaphore")

Si je pose et je libère mon sémaphore au sein de la même méthode, j'élimine pratiquement tout risque de sémaphore posé et jamais libéré. Un sémaphore non libéré peut provoquer un blocage d'une partie de la base allant jusqu'à forcer l'administrateur à redémarrer le 4D Server.
Ce genre de mésaventure peut conduire à des situations où nos utilisateurs nous traitent de développeurs d'eau douce ou encore de mérinos mal peignés. Je vais plus loin en m'efforçant d'avoir autant d'occurrences de chaque commande Semaphore et EFFACER SEMAPHORE, ce qui m'interdit de fait de mettre une des commandes dans une structure conditionnelle sans que l'autre y soit.
Enfin, vous vous doutez bien qu'un sémaphore est fortement lié à un processus (un process dans le langage 4D) ; il est donc inutile d'essayer de libérer dans le process B un sémaphore posé par le process A : 4D sera poli, mais en son fort intérieur considérera cela comme du code de Jocrisse.

En cherchant à réduire au maximum le code protégé par le sémaphore, je travaille pour la fluidité de mon application et éviter que le sémaphore soit un goulot d'étranglement. Si l'importance de la protection est indéniable, il ne faut pas que cela soit au détriment de la vitesse de l'application. A trop mettre "ceinture et brettelles", c'est à dire en surprotégeant son code on finit par faire du code d'amiral de bateau-lavoir.

Dans le code assez ancien que j'ai l'occasion régulièrement de rencontrer je vois souvent les lignes suivantes :

 

Tant que (Semaphore("MonSemaphore"))
    ENDORMIR PROCESS(Numero du process courant;500)
    APPELER 4D
Fin tant que


A première vue ce code est équivalent à celui que je propose un peu plus haut. Regardons y de plus prêt !
Si le sémaphore n'est pas disponible alors le process 4D s'endort pour 500 ticks et tente à nouveau sa chance 500 ticks plus tard. Malheureusement, pendant ce temps de sommeil, le sémaphore a pu être disponible, puis à nouveau pris par un autre process 4D concurrent. La solution souvent proposée pour éviter ce phénomène pénalisant est de réduire le temps de sommeil pour augmenter les chances. Malheureusement, cela ne fait qu'augmenter le traffic en client-serveur sans apporter de solution.
La vraie solution est d'utiliser le second paramètre optionnel de la commande Semaphore qui permet de dire à 4D :

  • je suis prêt à attendre jusqu'à un nombre de ticks (dans mon exemple 500) que le sémaphore soit disponible et cela sans que l'exécution du code passe à la ligne suivante
  • si dans le délai imparti le sémaphore est libéré, alors attribue-le moi de suite et reprend l'exécution du code
  • si dans le délai imparti le sémaphore n'est pas libéré, alors reprend l'exécution du code

Dans ce cas la demande de sémaphore attend le temps minimum et non plus un temps fixe ; le gain n'est pas seulement au niveau du temps d'attente, car la commande réalise également une priorisation des demandes en mettant en place une file d'attente. Ainsi le premier processus demandant un sémaphore sera le premier à l'obtenir.
Vous allez me demander pourquoi je ne profite pas au maximum de la file d'attente en mettant un temps beaucoup plus long que les 500 ticks donnés en exemple ?

Ma réponse est assez simple : je tiens à respecter ma règle numéro 2 et j'estime que 500 ticks est déjà long (je ne mets pas toujours ce chiffre). Ce choix de temporisation me permet de détecter les éventuels goulots d'étranglement en analysant les logs de 4D accessibles aux développeurs. Ainsi si je détecte que le nombre d'appels à la commande Semaphore est largement supérieur au nombre pour la commande EFFACER SEMAPHORE, je pourrais en déduire que j'ai un point d'attente créé par ma protection par sémaphore et que mon code n'est pas assez rapide. Si j'avais un temps beaucoup plus long, je ne serais pas en mesure de détecter cet éventuel problème. Comme toujours cela est une question d'équilibre entre les temps d'attente acceptables et la temporisation choisie ... et ce point d'équilibre n'est pas dans tous les cas obtenu du premier coup !

Et la commande Tester Semaphore ?
En théorie cette commande permet de tester si un sémaphore est déjà attribué ou non. Personnellement, je ne l'utilise jamais et ne la rencontre que très rarement lors de mes analyses de bases 4D. Pourquoi cette mise à l'écart ?

La première raison est certainement historique car cette commande n'est apparue qu'en version 6.5. Ma raison personnelle est que je n'éprouve jamais le besoin de tester un sémaphore sans l'utiliser. Il serait très illusoire d'utiliser la commande Tester semaphore pour s'assurer de l'existence ou non d'un autre processus utilisant le sémaphore. En effet un sémaphore étant destiné à être très bref pour les raisons évoquées précédemment, le temps de tester et d'en tirer les conséquences adéquates, j'ai toutes les chances que l'état du sémaphore ait changé, ou que le processus l'utilisant ne soit plus le même.
Il y a cependant un cas où l'on peut faire confiance à la commande Tester semaphore : Si vous protégez, par exemple, la clôture annuelle de votre comptabilité (ou tout autre opération longue, voire très longue) alors l'utilisation de Tester semaphore permet de changer l'interface en conséquence pour interdire l'accès à certaines opérations comme l'ajout de données comptables !

Avec les sémaphores il est très facile de mettre des macchabées d'eau de vaisselle dans son code : en effet le nom des sémaphores est diacritique et case-critique ce qui est assez rare dans 4D. Autrement dit votre sémaphore "Cercopithèque" n'est pas le même que "cercopitheque" ... attention donc à la casse et aux accents.

Il existe d'autres manières d'utiliser les sémaphores comme par exemple l'exemple de la documentation suivant :
 

Si (Semaphore ("MAJPrix"))  ` Essai de création du sémaphore
      ALERTE ("Un autre utilisateur est déjà en train de mettre à jour les prix. Essayez plus tard.")
Sinon
      MAJdesPrix ` Méthode de mise à jour des prix
      EFFACER SEMAPHORE("MAJPrix"))  ` Effacer le sémaphore
Fin de si

Ce code est parfaitement valide, mais suppose que l'opération demandée peut ne pas être réalisée. Cela demande donc une interface pour informer l'utilisateur de l'échec. On réservera ce genre de construction aux situations qui permettent donc un échec, ce qui est somme toute rarement le cas ...

Enfin n'oubliez pas que les sémaphores peuvent être locaux ou globaux. Les sémaphores locaux ont une portée limitée à un poste client ou au serveur et leur nom commencent par un $. Les sémaphores globaux sont vus de l'ensemble des postes du réseau qu'ils soient clients ou serveur.

Un bon sémaphore bien posé et utilisé assure une parfaite protection du code, Tonnerre de Brest !

 

RSS 16 commentaire(s) pour ce billet
Très bien ton article Olivier ... Perso, parfois je peste pour une seule chose sur la sémaphore .... Elle est posée Ok 4D me renvoi True mais par qui !!! Dans le LOCKED BY" au moins on connait le responsable (poste, Process, ...)
Merci pour ce cours ... d'injures ;-) C'est vrai que je crains toujours de l'utiliser du fait du risque de plantage ... mais c'est sûrement un tort pour des traitements courts et sensibles.

@Thierry : Qu'entends-tu par "risque de plantage" ?

@ Jean-François : Effectivement on ne peut pas savoir qui pose le sémaphore mais personnellement cela ne me gène pas plus que ça dans mon usage du sémaphore car je l'utilise principalement pour des actions atomiques très rapides ...

Dans le cas où l'information est nécessaire (savoir qui a posé le sémaphore), il est possible de mettre le nom de l'utilisateur dans une variable interprocess (protégée par le dit sémaphore) que l'on lit en cas de besoin ...

@Olivier : "risque de plantage" = risque que le client se ferme inopinément avant d'avoir effacer le sémaphore. Mais il est vrai que ce risque est faible, et d'autant plus faible si le traitement est court et le code bien écrit :-)

@Thierry. Nous avons deux cas possibles :

1) Le sémaphore est local et dans ce cas le crash du client est transparent pour les autres postes qui ne voyaient pas le sémaphore ...

2) Le sémaphore est global, c'est à dire visible par tous les postes clients et le serveur. Dans ce cas, si le client quitte inopinément alors le serveur se charge de libérer le sémaphore.

 

Voici une méthode de test que je propose :

 

  ` Méthode : TestSemaphore
$i:=0
Tant que (Semaphore("test";50))
    APPELER 4D
    $i:=$i+1
    MESSAGE(Chaine($i))
Fin tant que

ALERTE("Je suis dans la partie protégée")

EFFACER SEMAPHORE("test")

Cette méthode pose une alerte lorsqu'on obtient le sémaphore et propose un compteur si l'on est en attente du sémaphore. Ce code de test très basique permet de lance un premier poste et de rester sur l'alerte. Puis on lance un second poste et logiquement on obtient le compteur. On retourne sur le premier poste pour provoquer la fin brutale du client (via le gestionnaire des taches sous windows, ou en faisant un "forcer à quitter" sous mac) et l'on voit alors l'alerte apparaitre immédiatement sur le second poste.

 

Bref cela se passe très bien, et fort heureusement !

@Olivier : OK, alors je faisais fausse route. Merci pour l'info et autant pour moi. Dernière question. "Un sémaphore non libéré peut provoquer un blocage d'une partie de la base allant jusqu'à forcer l'administrateur à redémarrer le 4D Server." Dans quel cas cela peut-il se produire alors ?

@Thierry : Cela arrive, par exemple, si la commande Sémaphore est appelée dans un process  sans que la commande EFFACER SEMPHORE soit appelée.

 

Je suis assez d'accord avec l'article sur les sémaphore :-) En ce qui concerne le nom de sémaphore, je le défini dans une variable locale (ça évite les typos). De mémoire il est limité à 31 caractères. Enfin, dans les anciennes version, la tempo (2eme paramètre optionnel de la commande Semaphore), n'a pas d'effet dans le process principal (on ne peut pas endormir le process principal). Et je sais qu'on ne doit pas utiliser le process principal, mais quand on écrit du code dans un composant par exemple, on est jamais trop sur comment ça sera utilisé. Mon code ressemble à ça : C_TEXTE($vt_semaphoreName) $vt_semaphoreName:="$mySemaphore" Si (Non(UTL_semaphoreSet ($vt_semaphoreName;60)))  ` Attendre 1 secondes si un sémaphore existe déjà `Zone d'exclusion UTL_semaphoreClear ($vt_semaphoreName)  ` Effacer le sémaphore Sinon `Gestion d'erreur Fin de si Ouala

@Bruno : Il est vrai qu'il est préférable de ne pas utiliser le process principal ... par contre il est possible, en v11, d'endormir le process principal, et donc d'utiliser le second paramètre de la commande Semaphore sans contrainte !

Toujours en v11, le nom des sémaphores est limité à 255 caractères.

 

 

@Olivier : Merci pour ces précisions relatives à la v11 :-) Si ça continue comme ça j'aurais plus besoin de faire de code générique ni de wrappers ;-)
Bonjour Olivier, "Ce choix de temporisation me permet de détecter les éventuels goulots d'étranglement en analysant les logs de 4D accessibles aux développeurs." Je ne trouve pas ces logs en 4D v12 monoposte interprété, j'ai pourtant vérifié toutes les options de la base. Je n'ai rien de vraiment intéressant dans le dossier "logs" de ma base, pour suivre ces goulots. "Vous allez me demander pourquoi je ne profite pas au maximum de la file d'attente en mettant un temps beaucoup plus long que les 500 ticks donnés en exemple ?" et la question inverse : pourquoi pas, en monoposte, ne pas mettre un nombre de ticks beaucoup plus bas puisque l'on est dans une boucle ? En tout cas, merci beaucoup pour tous tes articles et conseils, que je consulte régulièrement, c'est vraiment très appréciable et l'on se rend compte chaque fois à quel point 4D est bien pensé ! :-)

@ Olivier V. : Les logs se déclenchent via la commande FIXER PARAMETRE BASE. En écrivant ces lignes je pensais aux logs des requêtes qui permet de compabiliser et suivre l'ensemble des échanges entre le serveur et les clients. Mais ce log ne donnera rien en monoposte ; ce n'est pas son rôle.

Pour votre question inverse : Si vous mettez un temps beaucoup plus bas, vous allez faire travailler beaucoup plus 4D pour rien. En effet, la temporisation dans la commande sémaphore n'est pas effectuée jusqu'au bout si le sémaphore se libère ; il n'est donc pas nécessaire de tester plus souvent pour aller plus vite.

Bonjour Olivier, Il est bien dommage que le nom du sémaphore soit diacritique et case-critique, puisque c'est tout à fait exceptionnel dans 4D. Comme je pose et supprime un sémaphore toujours dans la même méthode et par copier-coller et que souvent mes actions protégées s'exécutent dans un process qui meurt à la fin de l'action, je ne devrais pas avoir de pb. Il me semble pourtant avoir un bug en lien avec des sémaphores que je n'arrive pas à corriger depuis longtemps. Existe-t-il une possibilité de lister les sémaphore posés dans un code ou lors d'une exécution ?

 @ Registered user : Oui, il est possible d'avoir la liste en allant dans l'explorateur d'exécution, premier onglet, item "Sémaphore". Les sémaphores actifs (actuellement posés par un process) sont visibles

En fait, "Registered user", c'est moi … L'idée n'était pas d'avoir cette liste en temps réel, mais bien de l'enregistrer dans un log pour repérer visuellement ou par programmation les écarts d'accents (normalement je n'en met pas) ou les erreurs Minuscule-Majuscule (là je ne jurerais pas …)

Poster un nouveau commentaire

  • Les adresses de pages web et de messagerie électronique sont transformées en liens automatiquement.
  • You may use [view:viewname] tags to display listings of nodes.
  • Each email address will be obfuscated in a human readable fashion or (if JavaScript is enabled) replaced with a spamproof clickable link.

Plus d'informations sur les options de formatage