Les sémaphores et les signaux sont deux outils fournis par le langage 4D pour gérer les interactions et éviter les conflits entre les process dans une application multiprocess. Ils répondent à différents besoins :
Les sémaphores vous permettent de garantir que deux process ou plus ne modifient pas la même ressource (fichier, enregistrement, etc.) en même temps. Seul le process qui a posé le sémaphore peut le libérer.
Les signaux vous permettent de garantir qu'un ou plusieurs process attende(nt) qu'une tâche spécifique soit terminée avant de poursuivre son (leur) exécution. N'importe quel process peut attendre et/ou déclencher un signal.
Dans un programme informatique, un sémaphore est un outil permettant de protéger des actions qui ne doivent être réalisées que par un seul process ou utilisateur à la fois.
Dans 4D, le besoin classique d'usage d'un sémaphore est la modification d'un tableau interprocess : si un process est en train de modifier les valeurs du tableau, il ne faut pas qu'un autre process puisse faire la même chose en même temps. Le développeur utilise un sémaphore pour indiquer à un process qu'il ne peut réaliser la suite des opérations que si aucun autre process n'est déjà en train de réaliser les mêmes tâches. Lorsqu'un process rencontre un sémaphore, il y a trois possibilités :
il obtient immédiatement 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.
Le sémaphore permet donc de protéger des parties de code. Le sémaphore ne laisse passer qu'un process à la fois et interdit l'accès tant que le process détenteur du droit d'usage ne redonne pas son droit en libérant le sémaphore.
Dans 4D, vous posez un sémaphore en appelant la fonction Semaphore. Pour libérer un sémaphore, vous appelez la commande CLEAR SEMAPHORE.
La fonction Semaphore a un 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 retourne Vrai
si le sémaphore n'est pas attribué, la fonction attribue le sémaphore au process et répond Faux dans le même temps.
Ce principe de double action effectuée par la même commande permet de garantir qu'aucune opération extérieure ne peut s'insérer entre le test du sémaphore et son attribution.
La commande Test semaphore permet de savoir si un sémaphore est déjà attribué ou non. Cette commande est principalement utile dans le cadre d'opérations longues, comme par exemple la clôture annuelle de la comptabilité, où Test semaphore permet de contrôler l'interface pour interdire l'accès à certaines opérations telles que l'ajout de données comptables.
Il est recommandé d'utiliser les sémaphores en respectant les principes suivants :
un sémaphore doit être posé et libéré dans la même méthode,
l'exécution du code protégé par le sémaphore doit être la plus courte possible,
le code doit être temporisé à l'aide du paramètre nbTicks de la fonction Semaphore pour attendre la libération du sémaphore.
Voici le code type d'utilisation d'un sémaphore :
While(Semaphore("MonSemaphore";300)) IDLE End while // placer ici le code protégé par le sémaphore CLEAR SEMAPHORE("MonSemaphore")
Un sémaphore non libéré peut provoquer le blocage d'une partie de la base. Poser et libérer le sémaphore au sein de la même méthode permet pratiquement d'éliminer ce risque.
Réduire au maximum le code protégé par le sémaphore augmente la fluidité de l'application et évite que le sémaphore soit un goulot d'étranglement.
Enfin, l'utilisation du paramètre optionnel nbTicks de la commande Semaphore est indispensable pour optimiser l'attente de la libération d'un sémaphore. Avec ce paramètre, la commande fonctionne de la manière suivante :
le process attendra au maximum ce nombre de ticks (300 dans l'exemple) que le sémaphore soit disponible, sans que l'exécution du code passe à la ligne suivante,
si le sémaphore est libéré avant la fin du délai imparti, il est immédiatement attribué au process (Semaphore retourne Faux) et l'exécution du code reprend
si le sémaphore n'est pas libéré avant la fin du délai imparti, l'exécution du code reprend.
La commande réalise également une priorisation des demandes en mettant en place une file d'attente. Ainsi, le premier process demandant un sémaphore sera le premier à l'obtenir.
A noter que la durée d'attente est à régler en fonction des spécificités de l'application.
Il y a deux types de sémaphores dans 4D : les sémaphores locaux et les sémaphores globaux.
Un sémaphore local est visible par tous les process d'un même poste et seulement sur ce poste. Vous déclarez un sémaphore local en préfixant son nom avec le signe dollar ($). Les sémaphores locaux permettent de contrôler des opérations entre les différents process exécutés sur le même poste. Par exemple, un sémaphore local peut être utilisé pour gérer les accès à un tableau interprocess appelé par tous les process d'une base de données mono-utilisateur ou d'un poste client.
Un sémaphore global est visible par tous les utilisateurs et tous les process. Les sémaphores globaux permettent de contrôler des opérations entre les postes clients d'une base multi-utilisateurs.
Le principe de fonctionnement des sémaphores globaux et locaux est identique. Leur différence réside uniquement dans leur portée, c'est-à-dire leur visibilité. En client-serveur, les sémaphores globaux sont visibles pour tous les process de tous les postes clients et du serveur. Un sémaphore local n'est visible que pour les process du poste sur lequel il a été créé.
Avec 4D, les sémaphores globaux et locaux ont la même portée car il n'y a qu'un seul utilisateur. Cependant, si votre base est utilisée dans les deux environnements, n'hésitez pas à employer des sémaphores globaux et locaux, en fonction de vos besoins.
Note : Les sémaphores locaux sont recommandés lorsque l'usage d'un sémaphore est nécessaire pour gérer un aspect local à un client de l'application, comme par exemple l'interface ou un tableau de valeurs interprocess. L'utilisation d'un sémaphore global provoquerait dans ce cas non seulement des échanges réseau inutiles, mais en plus pourrait affecter inutilement d'autres postes clients. Le sémaphore local évitera ces effets indésirables.
doit être passé comme paramètre aux commandes qui appellent ou qui créent des workers ou des process.
Un worker/process qui appelle la méthode signal.wait( ) suspendra son exécution jusqu'à ce que la propriété signal.signaled soit mise à vrai. Lorsqu'il attend un signal, le process appelant n'utilise aucun cpu. Cela peut être très intéressant en matière de performance dans des applications multiprocess. La propriété signal.signaled est mise à vrai lorsqu'un worker/process appelle la méthode signal.trigger( ).
Notez que pour éviter tout blocage, signal.wait( ) peut également rendre la main à l'issue d'un timeout défini.
Le diagramme suivant illustre l'utilisation d'un objet signal :
Dans 4D, vous créez un nouvel objet signal en appelant la fonction New signal. Une fois créé, ce signal doit être passé comme paramètre aux commandes New process ou CALL WORKER afin qu'elles puissent le modifier après avoir accompli les tâches que vous souhaitez attendre.
Comme expliqué précédemment, le signal est un objet partagé qui contient deux méthodes intégrées :
signal.wait( ) doit être appelée depuis le worker/process qui requiert qu'un autre worker/process termine une tâche afin de poursuivre son action.
signal.trigger( ) doit être appelée depuis le worker/process qui a achevé son exécution afin de libérer les autres.
Une fois qu'un signal est libéré à l'aide de l'appel signal.trigger( ), il ne peut pas être réutilisé. Si vous souhaitez définir un autre signal, vous devez rappeler la fonction New signal.
Puisque signal est un objet partagé (voir Objets partagés et collections partagées), vous pouvez l'utiliser pour retourner les résultats issus des workers/process appelés, à condition d'écrire les valeurs dans une structure Utiliser...Fin utiliser (voir l'exemple).
Un process préemptif doit lire une valeur utilisateur (depuis un dialogue par exemple). Sachant qu'un dialogue ne peut pas être affiché à partir d'un process préemptif, ce dernier appelle un process coopératif et attend la valeur retournée :
// appeler le process principal et exécuter la méthode OpenForm CALL WORKER(1;"OpenForm";$signal) // effectuer d'autres calculs
... // Attendre la fin du process $signaled:=$signal.wait()
// Traitement des résultats $calc:=$signal.result+...
// Ajouter un nouvel attribut à votre objet signal partagé $signal pour passer vos résultats vers l'autre process : Use($signal) $signal.result:=$form.value End use
// Déclencher le signal au process en attente $signal.trigger()