Comment créer un sémaphore dans Bash – CloudSavvy IT
Agence web » Actualités du digital » Comment créer un sémaphore dans Bash –

Comment créer un sémaphore dans Bash –

Vous développez une application multi-thread ? Tôt ou tard, vous aurez probablement besoin d’utiliser un sémaphore. Dans cet article, vous apprendrez ce qu’est un sémaphore, comment en créer/implémenter un dans Bash, et plus encore.

Qu’est-ce qu’un Sémaphore?

Un sémaphore est une construction de programmation utilisée dans les programmes informatiques qui utilisent plusieurs threads de traitement (fils de traitement informatique qui exécutent chacun le code source du même programme ou de la même suite de programmes) pour obtenir l’utilisation exclusive d’une ressource commune à un moment donné . Dit d’une manière beaucoup plus simple, pensez à cela comme « un à la fois, s’il vous plaît ».

Un sémaphore a été défini pour la première fois vers la fin des années 1960 par le regretté informaticien Edsger Dijkstra de Rotterdam aux Pays-Bas. Vous avez probablement utilisé des sémaphores plusieurs fois dans votre vie sans vous en rendre compte spécifiquement !

Rester un peu aux Pays-Bas, un pays plein de petits cours d’eau et de nombreux ponts mobiles (appelés pont-levis en anglais américain), on peut voir de nombreux excellents exemples réels de sémaphore ; considérez le manche d’un opérateur de pont-levis : vers le haut ou vers le bas. Cette poignée est la variable sémaphore protégeant soit la voie navigable, soit la route des accidents. Ainsi, la voie navigable et les routes pourraient être considérées comme d’autres variables protégées par le sémaphore.

Si la poignée est relevée et que le pont est ouvert, l’usage exclusif de l’intersection eau/route est donné au ou aux navires traversant le canal d’eau. Lorsque la poignée est baissée, le pont est fermé, et l’usage exclusif de l’intersection eau/route est donné aux voitures qui passent sur le pont.

La variable sémaphore peut contrôler l’accès à un autre ensemble de variables. Par exemple, l’état haut de la poignée empêche le $cars_per_minute et $car_toll_booth_total_income variables d’être mises à jour, etc.

On peut pousser l’exemple un peu plus loin et préciser que lorsque la poignée est actionnée vers le haut ou vers le bas, une lumière correspondante est visible à la fois par les capitaines de navire sur l’eau et par les personnes conduisant des voitures et des camions sur la route : tous les opérateurs peuvent lire une variable commune pour un état particulier.

Alors que le scénario décrit ici n’est pas seulement un sémaphore, mais c’est aussi un simple mutex. Un mutex est une autre construction de programmation courante qui est très similaire à un sémaphore, avec la condition supplémentaire qu’un mutex ne peut être déverrouillé que par la même tâche ou le même thread qui l’a verrouillé. Mutex signifie « mutuellement exclusif ».

Dans ce cas, cela s’applique à notre exemple car l’opérateur de pont est le seul à contrôler notre sémaphore et notre poignée haut/bas de mutex. En revanche, si le poste de garde au bout du pont avait un interrupteur de contournement de pont, nous aurions toujours une configuration de sémaphore, mais pas un mutex.

Les deux constructions sont utilisées régulièrement dans la programmation informatique lorsque plusieurs threads sont utilisés pour garantir qu’un seul processus ou une seule tâche accède à une ressource donnée à tout moment. Certains sémaphores peuvent être à commutation ultra-rapide, par exemple lorsqu’ils sont utilisés dans un logiciel de trading multithread sur les marchés financiers, et certains peuvent être beaucoup plus lents, ne changeant d’état que toutes les quelques minutes, comme lorsqu’ils sont utilisés dans un pont-levis automatisé ou dans un passage à niveau de train routier.

Maintenant que nous avons une meilleure compréhension des sémaphores, implémentons-en un dans Bash.

Implémenter un sémaphore dans Bash : facile ou pas ?

L’implémentation d’un sémaphore dans Bash est si simple qu’elle peut même être effectuée directement à partir de la ligne de commande, du moins semble-t-il…

Commençons simplement.

BRIDGE=up
if [ "${BRIDGE}" = "down" ]; then echo "Cars may pass!"; else echo "Ships may pass!"; fi
BRIDGE=down
if [ "${BRIDGE}" = "down" ]; then echo "Cars may pass!"; else echo "Ships may pass!"; fi

Exemple de ligne de commande de mise à jour et de sortie de l'état du pont

Dans ce code, la variable BRIDGE détient notre statut de pont. Lorsque nous l’avons mis à up, les navires peuvent passer et lorsque nous le réglons sur down, les voitures peuvent passer. Nous pourrions également lire la valeur de notre variable à tout moment pour voir si le pont est vraiment en haut ou en bas. La ressource partagée/commune, dans ce cas, est notre pont.

Cependant, cet exemple est à thread unique, et nous n’avons donc jamais rencontréémaphore obligatoire situation. Une autre façon de penser à cela est que notre variable ne peut jamais être up et down exactement au même moment où le code est exécuté séquentiellement, c’est-à-dire étape par étape.

Une autre chose à noter est que nous n’avons pas vraiment contrôlé l’accès à une autre variable (comme le ferait habituellement un sémaphore), et donc notre BRIDGE variable n’est pas vraiment une vraie variable sémaphore, bien qu’elle s’en rapproche.

Enfin, dès que nous introduisons plusieurs threads qui peuvent affecter le BRIDGE variable, nous rencontrons des problèmes. Par exemple, et si, juste après la BRIDGE=up commande, un autre problème de thread BRIDGE=down ce qui entraînerait alors le message Cars may pass! sortie, même si le premier thread s’attendrait à ce que le pont soit up, et en réalité le pont bouge toujours. Dangereux!

Vous pouvez voir comment les choses peuvent rapidement devenir troubles et confuses, pour ne pas dire complexes, lorsque vous travaillez avec plusieurs threads.

La situation dans laquelle plusieurs threads essaient de mettre à jour la même variable soit en même temps, soit au moins suffisamment près dans le temps pour qu’un autre thread se trompe de situation (ce qui, dans le cas des ponts-levis, peut prendre un certain temps) s’appelle un condition de course: deux threads se précipitent pour mettre à jour ou signaler une variable ou un état, avec pour résultat qu’un ou plusieurs threads peuvent se tromper.

Nous pouvons rendre ce code bien meilleur en le transférant dans des procédures et en utilisant une vraie variable sémaphore qui restreindra l’accès à notre BRIDGE variable selon la situation.

Créer un sémaphore bash

Implémenter un multithread complet, qui est thread-safe (un terme informatique pour décrire un logiciel qui est thread-safe ou développé de telle manière que les threads ne peuvent pas s’affecter négativement/incorrectement alors qu’ils ne le devraient pas) n’est pas une tâche facile. Même un programme bien écrit qui utilise des sémaphores n’est pas garanti d’être entièrement thread-safe.

Plus il y a de threads et plus la fréquence et la complexité des interactions de threads sont élevées, plus il est probable qu’il y aura des conditions de concurrence.

Pour notre petit exemple, nous examinerons la définition d’un sémaphore Bash lorsqu’un des opérateurs de pont-levis abaisse une poignée de pont, indiquant ainsi qu’il souhaite abaisser le pont. Les lecteurs avides ont peut-être remarqué la référence à opérateurs à la place de opérateur: il y a maintenant plusieurs opérateurs qui peuvent abaisser le pont. En d’autres termes, il existe plusieurs threads ou tâches qui s’exécutent tous en même temps.

#!/bin/bash

BRIDGE_SEMAPHORE=0

lower_bridge(){  # An operator put one of the bridge operation handles downward (as a new state).
  # Assume it was previously agreed between operators that as soon as one of the operators 
  # moves a bridge operation handle that their command has to be executed, either sooner or later
  # hence, we commence a loop which will wait for the bridge to become available for movement
  while true; do
    if [ "${BRIDGE_SEMAPHORE}" -eq 1 ]; then
      echo "Bridge semaphore locked, bridge moving or other issue. Waiting 2 minutes before re-check."
      sleep 120
      continue  # Continue loop
    elif [ "${BRIDGE_SEMAPHORE}" -eq 0 ]; then   
      echo "Lower bridge command accepted, locking semaphore and lowering the bridge."
      BRIDGE_SEMAPHORE=1
      execute_lower_bridge
      wait_for_bridge_to_come_down
      BRIDGE='down'
      echo "Bridge lowered, ensuring at least 5 minutes pass before next allowed bridge movement."
      sleep 300
      echo "5 Minutes passed, unlocking semaphore (releasing bridge control)"
      BRIDGE_SEMAPHORE=0
      break  # Exit loop
    fi
  done
}

Une implémentation de sémaphore dans Bash

Ici, nous avons un lower_bridge fonction qui fera un certain nombre de choses. Tout d’abord, supposons qu’un autre opérateur a récemment déplacé le pont dans la dernière minute. En tant que tel, il existe un autre thread exécutant du code dans une fonction similaire à celle-ci appelée raise_bridge.

En fait, cette fonction a fini de lever le pont mais a institué une attente obligatoire de 5 minutes sur laquelle tous les opérateurs se sont mis d’accord auparavant et qui a été codée en dur dans le code source : elle empêche le pont de monter/descendre tout le temps. Vous pouvez également voir cette attente obligatoire de 5 minutes implémentée dans cette fonction comme sleep 300.

Alors, quand ça raise_bridge fonction fonctionne, il aura défini la variable sémaphore BRIDGE_SEMAPHORE à 1, tout comme nous le faisons dans le code ici (directement après le echo "Lower bridge command accepted, locking semaphore and lowering bridge" commande), et – au moyen de la première if vérification conditionnelle dans ce code – la boucle infinie présente dans cette fonction continuera (ref continue dans le code) en boucle, avec des pauses de 2 minutes, au fur et à mesure BRIDGE_SEMAPHORE la variable est 1.

Dès que ça raise_bridge la fonction termine de lever le pont et de terminer ses cinq minutes de sommeil, elle définira le BRIDGE_SEMAPHORE à 0, permettant à notre lower_bridge fonction co commencer à exécuter les fonctions execute_lower_bridge Et subséquente wait_for_bridge_to_come_down tout en ayant d’abord re-verrouillé notre sémaphore pour 1 pour empêcher d’autres fonctions de prendre le contrôle du pont.

Il y a cependant des lacunes dans ce code, et des conditions de concurrence qui peuvent avoir des conséquences de grande envergure pour les opérateurs de ponts sont possibles. Pouvez-vous en repérer ?

le "Lower bridge command accepted, locking semaphore and lowering bridge" n’est pas thread-safe !

Si un autre fil, par exemple raise_bridge s’exécute en même temps et essaie d’accéder au BRIDGE_SEMAPHORE variable, il pourrait être (quand BRIDGE_SEMAPHORE=0 et les deux threads en cours d’exécution atteignent leur echoc’est exactement au même moment que les pontiers voient « Commande de pont inférieur acceptée, sémaphore de verrouillage et pont inférieur » et « Commande de levée de pont acceptée, verrouillage du sémaphore et levée de pont ». Directement après l’autre sur l’écran ! Effrayant, non ?

Plus effrayant encore est le fait que les deux fils peuvent procéder à BRIDGE_SEMAPHORE=1, et les deux threads peuvent continuer à s’exécuter ! (Rien ne les empêche de le faire) La raison en est qu’il n’y a pas encore beaucoup de protection pour de tels scénarios. Bien que ce code implémente un sémaphore, il n’est donc en aucun cas thread-safe. Comme indiqué, le codage multithread est complexe et nécessite beaucoup d’expertise.

Bien que le temps requis dans ce cas soit minime (1 à 2 lignes de code ne prennent que quelques millisecondes à exécuter), et étant donné le nombre probablement faible d’opérateurs de pont, la possibilité que cela se produise est très faible. Cependant, le fait qu’il soit possible est ce qui le rend dangereux. Créer du code thread-safe dans Bash n’est pas une tâche facile.

Cela pourrait être encore amélioré en introduisant, par exemple, un pré-verrouillage et/ou en introduisant une certaine forme de délai avec une re-vérification ultérieure (bien que cela nécessitera probablement une variable supplémentaire) ou en effectuant une re-vérification régulière avant l’exécution réelle du pont. , etc. Une autre option consiste à créer une file d’attente prioritaire ou une variable de compteur qui vérifie combien de threads ont verrouillé le contrôle du pont, etc.

Une autre approche couramment utilisée, par exemple, lors de l’exécution de plusieurs scripts bash qui pourraient interagir, consiste à utiliser mkdir ou alors flock comme opérations de verrouillage de base. Il existe divers exemples de mise en œuvre disponibles en ligne, par exemple, Quelles commandes Unix peuvent être utilisées comme sémaphore/verrouillage ?.

Emballer

Dans cet article, nous examinons ce qu’est un sémaphore. Nous avons également brièvement abordé le sujet d’un mutex. Enfin, nous avons examiné la mise en œuvre d’un sémaphore dans Bash en utilisant l’exemple pratique de plusieurs opérateurs de pont exploitant un pont mobile/pont-levis. Nous avons également exploré la complexité de la mise en œuvre d’une solution fiable basée sur des sémaphores.

Si vous avez aimé lire cet article, jetez un œil à nos assertions, erreurs et plantages : quelle est la différence ? article.

★★★★★