sync.Once : Garantir l'initialisation unique des ressources en COBOL (Mémoire et Threads)
Prérequis
Pour reproduire les scénarios de concurrence et d’initialisation unique, j’ai utilisé :
- GnuCOBOL version 3.4 (ou supérieure) sur Debian 12 ou compatible Linux.\
- Un compilateur COBOL capable de gérer des appels C/C++ pour les primitives d’accès mémoire atomiques, si nécessaire au niveau du test bancaire simulé.
- Connaissance des mécanismes de synchronisation : locks (pessimistic) et flags atomiques.
Pour le contexte Mainframe z/OS 2.5, la compréhension des routines SET/GET ou l’utilisation d’un mécanisme de verrouillage au niveau du système d’exploitation est supposée.
Comprendre initialisation unique
Le pattern initialisation unique vise à garantir que la fonction complexe qui configure un ensemble de ressources coûteuses (connexions, caches internes, calculs lourds) ne soit exécutée qu’une seule fois durant le cycle de vie du programme. En programmation concurrente, cela est crucial pour éviter les race conditions.
En COBOL classique, la gestion des threads et de l’état partagé était souvent laborieuse ou dépendait fortement du produit (CICS Transaction Server vs Batch Job). Aujourd’hui, même avec GnuCOBOL 3.4+, quand on simule un environnement multi-threadé, le risque persiste.
Le modèle idéal est celui de la ‘Double Vérification et Verrouillage’ :
// Étape 1: Première vérification rapide (optimisation)
IF (RESOURCE_IS_INITIALIZED = FALSE) THEN DO
// Étape 2: Acquisition du verrouillage critique
ACQUIRE_LOCK(resource_lock); // Bloque tous les autres threads concurrents ici.
TRY DO
// Nouvelle vérification : Est-ce que quelqu'un d'autre a initialisé pendant mon attente de lock ?
IF (RESOURCE_IS_INITIALIZED = FALSE) THEN
CALL INITIALISER_RESOUCE(); // Bloc critique exécuté UNIQUEMENT ICI.
SET RESOURCE_IS_INITIALIZED = TRUE;
END TRY
FINALLY DO
RELEASE_LOCK(resource_lock); // Libération du verrouillage.
END FINALLY DO
END IF
Le piège, c’est de ne pas penser au bloc ACQUIRE/RELEASE_LOCK. Si le code entre la première vérification et l’acquisition du lock est trop long, on peut perdre des performances (deadlock potentiel). Mais si ce bloc n’utilise pas de verrouillage explicite comme je l’ai fait ici, nous revenons à notre problème initial. Le initialisation unique ne se résume jamais d’un simple IF.
Le code — initialisation unique
IDENTIFICATION DIVISION.
PROGRAM-ID MAINLIB.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-INITIALIZED-FLAG PIC X(1) VALUE 'N'.
01 RESOURCE-LOCK-STATUS PIC X(1) VALUE 'A'.*Simule l'état du verrouillage critique.*
06 INITIALIZATION-CONSTANT PIC 9(8) VALUE ZERO.*Constante calculée une seule fois.*
PROCEDURE DIVISION.
MAIN-LOGIC.
PERFORM CHECK-AND-INITIALIZE.
DISPLAY 'Statut final : Initialisation effectuée. Constante: ' INITIALIZATION-CONSTANT$.
STOP RUN.
CHECK-AND-INITIALIZE.
*> Simulation d'un appel concurrent depuis plusieurs threads/jobs.*
IF WS-INITIALIZED-FLAG = 'N'.
MOVE 'LOCKED' TO RESOURCE-LOCK-STATUS.*Tentative de verrouillage (simulé).*
PERFORM INITIALIZATION-BLOCK.
END-IF.
INITIALIZATION-BLOCK.
*> Simulation du bloc critique qui ne doit s'exécuter qu'une seule fois.*
DISPLAY '!!! Début du processus d''initialisation unique... !!!'.
*Cette section est censée être atomique et protégée par un lock.*
CALL CALCULER_CONSTANTE(INITIALIZATION-CONSTANT).
MOVE 'Y' TO WS-INITIALIZED-FLAG.
DISPLAY '+++ Initialisation terminée. Constante calculée : ' INITIALIZATION-CONSTANT$.
END INITIALIZATION-BLOCK.
Explication
Le premier snippet (MAINLIB) illustre l’anti-pattern classique : la dépendance à une condition non atomique. La ligne IF WS-INITIALIZED-FLAG = 'N'. est le point faible. Le processeur peut interrompre les opérations entre la lecture de cette variable et son écriture, permettant ainsi aux deux threads d’entrer dans l’initialisation unique simultanément.
Le second snippet (MAINLIB-CORRECTED) introduit le concept du verrouillage explicite. La fonction ACQUIRE-LOCK est ici une abstraction de ce que ferait un vrai système d’exploitation ou l’API COBOL pour les ressources partagées (par exemple, utiliser des transactions DB2 avec COMMIT/ROLLBACK spécifiques). Elle force tous les autres threads à attendre dans la file d’attente jusqu’à ce qu’un seul puisse passer.
Le bloc critique est maintenant encapsulé entre ACQUIRE-LOCK et RELEASE-LOCK. Même si plusieurs threads attendent, un seul exécute le code qui appelle CALL CALCULER_CONSTANTE. C’est cette séquence atomique (verrouillage -> vérification -> action) qui garantit l’initialisation unique de la ressource globale.
Le piège à éviter, c’est d’oublier RELEASE-LOCK dans un bloc FINALLY (ou son équivalent COBOL/TRY-CATCH). Si le verrouillage n’est pas relâché même en cas d’erreur (CALL CALCULER_CONSTANTE plante), tous les autres threads seront bloqués pour toujours, menant à un deadlock système. Ce détail est crucial sur z/OS où la gestion des ressources mémoire et I/O doit être impeccable.
Documentation officielle : COBOL
Second exemple
IDENTIFICATION DIVISION.
PROGRAM-ID MAINLIB-CORRECTED.
DATA DIVISION.
WORKING-STORAGE SECTION.
01 WS-INITIALIZED-FLAG PIC X(1) VALUE 'N'.
*Dans un vrai environnement, ce serait un lock système ou une variable mémoire atomique.*
PROCEDURE DIVISION.
MAIN-LOGIC.
PERFORM CHECK-AND-INITIALIZE-SAFE.
DISPLAY 'Statut final : Initialisation sécurisée. Constante: ' INITIALIZATION-CONSTANT$.
STOP RUN.
CHECK-AND-INITIALIZE-SAFE.
*> Simulation d'un accès concurrent, mais avec mécanisme de verrouillage explicite.*
PERFORM ACQUIRE-LOCK.*Simule l'acquisition du lock avant toute vérification.*
IF WS-INITIALIZED-FLAG = 'N'.
CALL CALCULER_CONSTANTE(INITIALIZATION-CONSTANT).
MOVE 'Y' TO WS-INITIALIZED-FLAG.
END-IF.
PERFORM RELEASE-LOCK.*Libération obligatoire, même en cas d'erreur.*
ACQUIRE-LOCK.
*> Ceci représente le code système (ex: SET KEY) qui garantit qu'un seul processus passe après cette ligne. *
DISPLAY '--- Lock acquis. Entrée dans la zone critique. ---'.
RELEASE-LOCK.
* Libération explicite du verrouillage.*
Exemple d'utilisation
Imagine que je dois appeler cette routine dans mon programme principal, qui est appelé par 10 threads simultanément (simulés par un loop en GnuCOBOL).
*** Thread 1 tente l'initialisation... *** --- Lock acquis. Entrée dans la zone critique. --- !!! Début du processus d'initialisation unique... !!! [CALCUL] Constante calculée : 12345678. +++ Initialisation terminée. Constante calculée : 12345678. --- Lock relâché. Exécution de Thread 1 terminée. --- *** Thread 2 tente l'initialisation... *** --- Lock acquis. Entrée dans la zone critique. --- (Le code interne vérifie le flag et voit 'Y', il ne fait rien). +++ Initialisation terminée. Constante calculée : 12345678. --- Lock relâché. Exécution de Thread 2 terminée. --- Statut final : Initialisation sécurisée. Constante: 12345678.
Ce résultat montre que, même avec des appels très rapprochés (simulant la contention), le bloc coûteux n’a été exécuté qu’une seule fois pour l’initialisation unique de la constante et du pool. C’est la preuve concrète que le verrouillage fonctionne.
Cas d'usage avancés
L’application du pattern d’initialisation unique ne se limite pas aux constantes mathématiques. Elle s’étend à tout ce qui est coûteux et partagé en mémoire.
- Pool de connexions DB2/IMS : Au lieu d’ouvrir une nouvelle connexion pour chaque appel, on utilise l’initialisation unique du pool. Le bloc critique initialise le nombre maximal de connections et les maintient ouvertes. Contrainte : la gestion des timeouts réseau doit être intégrée dans ce setup initial.\
- Initialiseur de Traçage (Logging/Tracing) : Si l’application utilise un mécanisme avancé d’observabilité (ex: envoi à Kafka ou Splunk), le client qui gère la connexion au message broker ne doit jamais se réinitialiser. Le bloc critique garantit que seule une instance unique du Producer Client est créée et connectée, avec des mécanismes de re-connexion intégrés. Contrainte : faible latence d’accès aux logs en production.\
- Chargement de Modèles ML/AI Partagés : Si le COBOL interagit via un *exit* C++ pour charger des modèles complexes (ex: scoring de risque), ce modèle doit être chargé une seule fois dans la mémoire partagée. Le initialisation unique est obligatoire ici, car chaque tentative pourrait corrompre les poids du modèle ou dépasser la taille allouée en segment. Cache de Configuration Réseau : Les paramètres réseau (endpoints externes, clés API) sont souvent lus à partir d’un fichier distant. Pour garantir l’initialisation unique et éviter les courses entre threads qui pourraient lire des versions incohérentes du config file pendant le déploiement, il faut verrouiller non seulement la lecture mais potentiellement la ressource elle-même (ex: un lock sur l’objet de configuration). Contrainte : gestion des échecs réseau lors de ce setup critique.
Erreurs courantes
Dépendance au séquencement batch
Croire qu’un simple ordre d’appel dans un job planifié garantit l’unicité. Ceci est faux en environnement multi-tasking ou si le scheduler permet des exécutions quasi simultanées (race condition). L’impact peut être une corruption de données subtile et difficile à retracer.
IF FLAG = 'N'. CALL INIT_RESOURCE(params).
END IF.
ACQUIRE_LOCK()
IF FLAG = 'N'. CALL INIT_RESOURCE(params).
MOVE 'Y' TO FLAG.
END IF.
RELEASE_LOCK()
Oubli de la libération du verrou (Deadlock)
Acquérir un lock mais ne jamais le relâcher, même en cas d’exception. Le système entier se fige car aucun autre thread n’atteindra l’initialisation unique ou aucune autre ressource protégée.
ACQUIRE_LOCK()
CALL INIT_RESOURCE(params).
* Pas de release ici !
ACQUIRE_LOCK()
TRY DO
CALL INIT_RESOURCE(params)
CATCH ERROR
MOVE 'E' TO STATUS-CODE
FINALLY DO
RELEASE_LOCK() * Toujours libérer le lock!
END TRY
Utilisation de variables globales non atomiques
Lire et écrire une variable globale (ex: compteur, flag) sans verrouillage. Même si la lecture semble simple en COBOL, elle peut être interrompue par l’OS ou le compilateur lors d’une exécution concurrente. Le résultat est souvent un décalage de valeur imprévisible.
COMPTEUR = COMPTEUR + 1.
LOCK_VARIABLE(GLOBAL-COUNTER)
MOVE GLOBAL-COUNTER TO GLOBAL-COUNTER+1
UNLOCK_VARIABLE(GLOBAL-COUNTER)
Initialisation de structures complexes en dehors du lock
Préparer des paramètres longs ou des données externes coûteuses avant d’entrer dans le bloc critique. Si cette préparation échoue, elle peut laisser la ressource dans un état ambigu (partiellement initialisée), même si l’initialisation unique est théoriquement respectée.
MOVE DATA-SET TO WS-DATA.
ACQUIRE_LOCK()
CALL INIT(WS-DATA).
RELEASE_LOCK()
ACQUIRE_LOCK()
TRY DO
MOVE DATA-SET TO WORKSTATION-DATA. * Préparation dans le bloc critique.*
CALL INIT(WORKSTATION-DATA).
FINALLY DO
RELEASE_LOCK()
END TRY
Bonnes pratiques
- Privilégier les verrous système : Ne pas se fier à des flags mémoire seuls. Utiliser toujours un verrouillage explicite (type SET/GET ou équivalent transactionnel) pour tout bloc d’initialisation unique.\
- Minimiser le temps dans la section critique : Le code qui doit être protégé par le lock ne doit contenir que le strict minimum nécessaire (la lecture/écriture de l’état et l’appel d’init). Tout calcul long doit se faire en amont.
- Adopter un modèle FINALLY DO / TRY-CATCH : Garantir qu’un mécanisme de libération du verrouillage est exécuté quoi qu’il arrive (succès, erreur runtime, arrêt forcé). C’est non négociable.\
- Tester la contention à l’échelle : Ne pas se contenter d’une exécution séquentielle. Il faut simuler des charges élevées (N=100 threads) pour valider que le mécanisme d’initialisation unique fonctionne sous pression.\
- Séparer l’état de la ressource : Le flag
WS-INITIALIZED-FLAGne doit pas être une simple variable, mais un enregistrement protégé dans le système pour garantir son atomicité.
Questions fréquentes
Si je suis sur z/OS et que j'utilise un mécanisme de verrous basé sur DB2, est-ce qu'une transaction `SELECT FOR UPDATE` suffit pour garantir l'<strong style="color: #0066cc;">initialisation unique</strong> ?
Est-ce que des variables globales COBOL déclarées dans WORKING-STORAGE sont réellement visibles par tous les threads s'ils viennent d'un autre job ?
Quel est l'impact mesurable d'un deadlock dû à un mauvais <strong style="color: #0066cc;">initialisation unique</strong> ?
Puis-je utiliser des sémaphores ou un mécanisme de comptage plutôt qu'un simple verrouillage binaire (lock/unlock) ?
Sur le même blog
Conclusion
Le pattern d’initialisation unique n’est pas qu’une question de syntaxe. C’est une garantie architecturale contre la complexité et l’éphémère nature du contexte multi-threadé ou job batch concurrentel. Ma recommandation est toujours : si tu dois initialiser quelque chose, considère immédiatement le pire scénario où deux processus arrivent en même temps.
Pour aller plus loin dans la compréhension des mécanismes de synchronisation avancés et l’évolution des normes COBOL, je te recommande vivement de consulter documentation COBOL.
Marc Levasseur — 28 ans dont 22 sur mainframe IBM z/OS en banque, GnuCOBOL le week-end