Le Cycle de Vie et les Fonctions d'Initialisation (init)
Le démarrage d'une application Go suit une séquence bien définie. Tout d'abord, les fonctions init du package runtime sont exécutées. Ensuite, les fonctions init des packages importés (bibliothèques standard, tierces et packages utilisateur) sont appelées. Enfin, la fonction main.main, le point d'entrée de l'application utilisateur, est invoquée.
Les fonctions init sont exécutées au moment du chargement du package. Dans le package main, l'ordre d'exécution est le suivant :
- Initialisation des variables globales.
- Exécution des fonctions
init. - Exécution de la fonction
main.
L'ordre d'exécution des multiples fonctions init peut varier. Pour un même fichier source, les fonctions init sont exécutées de haut en bas. Pour plusieurs fichiers Go dans le même package, l'ordre est déterminé par l'ordre ASCII des noms de fichiers (par exemple, a.go avant b.go). Entre différents packages, l'exécution se fait selon l'ordre d'importation et les dépendances : les packages dépendus sont initialisés avant ceux qui en dépendent. Cependant, un ordre déterministe absolu n'est pas garanti dans des graphes de dépendance complexes.
Les fonctions init sont spéciales : elles ne peuvent pas être appelées explicitement et ne peuvent pas accepter de paramètres.
Gestion de la Mémoire en Go
La structure de la mémoire en Go est organisée en trois zones principales : la zone des spans (environ 512 Mo), la zone de bitmap (environ 16 Go) et la zone d'arena (environ 512 Go). Les composants de gestion mémoire incluent mcache (pour les allocations sans verrouillage), mcentral et mheap. Un mspan est une liste doublement chaînée de pages de mémoire (chaque page mesure 8 Ko minimum), divisées en slots de taille fixe.
Mécanismes d'Allocation
- Les objets de taille supérieure à 32 Ko sont alloués directement depuis le
mheap. - Les petits objets (moins de 16 octets) sans pointeurs peuvent utiliser un allocateur "tiny" pour réduire le gaspillage, bien que cela puisse compliquer le processus de récupération.
- Les objets de moins de 16 octets avec pointeurs, ou ceux entre 16 et 32 Ko, obtiennent des slots de
mspanvia lemcachelocal au thread.
Les fonctions new et make sont utilisées pour l'allocation. new alloue de la mémoire pour un type et retourne un pointeur vers une valeur initialisée à zéro de ce type (par exemple, new(int) retourne un *int avec la valeur 0). make est utilisé pour allouer et initialiser des slices, des maps et des channels (par exemple, make([]int, 0, 10) pour une slice, make(map[string]int) pour une map, make(chan int, 5) pour un channel). Contrairement à new, make retourne une valeur initialisée du type spécifié, pas un pointeur.
Analyse d'Échappement (Escape Analysis)
Go utilise l'analyse d'échappement pour déterminer si une variable doit être allouée sur la pile ou sur le tas. Si une variable locale ou un paramètre d'une fonction "s'échappe" au-delà de sa portée (par exemple, si son adresse est retournée par la fonction), elle sera allouée sur le tas. Sinon, elle sera allouée sur la pile, ce qui est plus rapide à allouer et à désallouer. Éviter l'échappement peut améliorer les performances en réduisant la pression sur le ramasse-miettes.
Alignement Mémoire
L'alignement mémoire est essentiel pour optimiser les accès CPU. En Go, les structures (struct) sont alignées pour que chaque champ commence à une adresse qui est un multiple de sa taille, ou de la taille d'alignement maximale du système (souvent 8 octets), pour assurer un accès efficace.
Utilisations des Structures Vides (struct{})
Les structures vides struct{} n'occupent aucune mémoire (ou un octet symbolique pour des cas spécifiques comme le dernier champ d'une structure). Elles sont utiles pour :
- Construire des ensembles (sets) avec des maps (
map[T]struct{}). - Transmettre des signaux via des channels sans transmettre de données (
chan struct{}). - Comme marqueur sans coût mémoire.
Collecte de Garbages (GC)
Le ramasse-miettes de Go utilise un algorithme de marquage-balayage concurrent à trois couleurs, optimisé pour minimiser les pauses (Stop-The-World ou STW). Historiquement, Go 1.3 utilisait un simple marquage-balayage qui entraînait des pauses STW plus longues.
Algorithme à Trois Couleurs
Le GC de Go classifie les objets en trois couleurs :
- Blanc : Objets non visités. Cible potentielle pour la collecte.
- Gris : Objets visités, mais dont les références n'ont pas encore été explorées.
- Noir : Objets visités, et toutes leurs références ont été explorées. Considérés comme vivants.
Le processus commence avec tous les objets sur le tas en blanc. Le GC scanne les "racines" (variables sur la pile, variables globales) et marque en gris les objets directement accessibles. Il parcourt ensuite les objets gris, les marquant en noir une fois que toutes leurs références ont été marquées en gris. Les objets blancs restants après ce processus sont considérés comme non accessibles et sont balayés.
Barrières d'Écriture (Write Barriers)
Pour garantir la cohérence pendant la phase de marquage concurrente (où l'application continue de s'exécuter et modifie le graphe d'objets), Go utilise des barrières d'écriture :
- Barrière d'Insertion : Lorsqu'un objet noir référence un objet blanc, l'objet blanc est marqué en gris. Cela maintient la "condition de tri-couleur forte" (un objet noir ne pointe jamais vers un objet blanc).
- Barrière de Suppression : Lorsqu'une référence d'un objet gris ou blanc vers un objet blanc est supprimée, l'objet blanc est marqué en gris. Cela maintient la "condition de tri-couleur faible" (une chaîne de références d'un objet gris vers un objet blanc ne doit pas être rompue).
- Barrière d'Écriture Hybride : Go combine ces approches pour minimiser le STW final. Elle protège les objets marqués en début de cycle GC et garantit que les nouvelles allocations sont initialement noires, évitant ainsi un nouveau scan complet de la pile.
Déclencheurs du GC
Le GC peut être déclenché de plusieurs manières :
- Automatiquement lorsque le rapport d'utilisation de la mémoire atteint le seuil défini par
GOGC. - Par le moniteur système (
sysmon) en arrière-plan. - Manuellement via
runtime.GC().
Détection des Fuites Mémoire
Des outils comme pprof, trace et race sont essentiels pour détecter et analyser les fuites mémoire ou les problèmes de performence liés au GC.
Le Planificateur Go (Scheduler)
Le planificateur Go utilise le modèle GPM (Goroutine, Processor, Machine/Thread) pour gérer les goroutines de manière efficace et légère.
- G (Goroutine) : Une fonction exécutée de manière concurrente. C'est l'unité de travail du planificateur Go, bien plus légère qu'un thread OS (environ 2 Ko de pile extensible par défaut).
- P (Processor) : Une ressource logique qui peut exécuter des goroutines. Un P contient une file d'attente locale de goroutines (environ 256 G) prêtes à être exécutées. Le nombre de P est généralement égal au nombre de cœurs logiques CPU (contrôlé par
GOMAXPROCS). - M (Machine/Thread OS) : Un thread du système d'exploitation. Un M exécute le code Go des goroutines qui lui sont assignées par un P. Un M est responsable de l'exécution d'un P à la fois.
Mécanisme de Planification
Chaque P dispose d'une file d'attente locale de goroutines. Si cette file est vide, un P peut "voler" des goroutines à d'autres P ou à la file d'attente globale de goroutines. Lorsque toutes les files sont vides, les M peuvent passer en mode "spin" pour rechercher de nouvelles goroutines.
Quand une goroutine effectue une opération bloquante (appel système, E/S réseau bloquante, mutex bloquant), le M associé est également bloqué. Dans ce cas, le P est dissocié du M bloqué et associé à un nouveau M (si disponible) ou en crée un nouveau, permettant ainsi à d'autres goroutines de continuer à s'exécuter.
Communication entre Goroutines
Les goroutines communiquent principalement via les canaux (chan), suivant le principe "ne communiquez pas en partageant de la mémoire ; partagez de la mémoire en communiquant". Le partage de mémoire direct est possible mais nécessite des primitives de synchronisation comme les mutex.
Gestion des Panics et Contrôle des Goroutines
Si une goroutine panique et que cette panic n'est pas récupérée par recover, le programme entier s'arrête. Il n'existe pas de moyen direct de "tuer" une goroutine. Le contrôle de l'arrêt d'une goroutine (par exemple, suite à l'arrêt du parent ou un timeout) se fait via des mécanismes coopératifs, comme les canaux de signalisation ou le package context.
Pour limiter le nombre de goroutines concurrentes, un pattern courant utilise un channel tamponné comme un sémaphore : la capacité du channel détermine le nombre maximum de goroutines actives.
Canaux (Channels)
Les canaux sont un moyen puissant et sûr de communication et de synchronisation entre goroutines en Go. Leur implémentation sous-jacente se trouve dans la structure hchan du package runtime.
Structure Interne d'un Channel
Un channel est représenté par la structure hchan, qui comprend notamment :
qcount: Nombre total d'éléments dans la file d'attente.dataqsiz: Taille du buffer circulaire (capacité).buf: Pointeur vers le buffer sous-jacent (pour les channels tamponnés).elemsize: Taille de chaque élément stocké dans le channel.closed: Indique si le channel est fermé.elemtype: Type des éléments du channel.sendx: Index de l'emplacement du prochain envoi dans le buffer.recvx: Index de l'emplacement de la prochaine réception dans le buffer.recvqetsendq: Files d'attente de goroutines en attente de recevoir ou d'envoyer.lock: Un mutex protégeant toutes les opérations sur la structurehchan, assurant la sécurité concurrente.
Comportement des Channels
| Opération | Channel nil |
Channel fermé | Channel non nil, non fermé |
|---|---|---|---|
close(ch) |
Panic | Panic | Fermeture normale |
<- ch (réception) |
Bloquant | Réception de la valeur zéro du type si vide, sinon des valeurs restantes | Bloquant ou réception normale. Bloque si channel tamponné est vide ou si non-tamponné n'a pas d'expéditeur en attente. |
ch <- val (envoi) |
Bloquant | Panic | Bloquant ou envoi normal. Bloque si channel non-tamponné n'a pas de récepteur en attente ou si channel tamponné est plein. |
Fermeture Élégante des Channels
Une règle d'or est de ne fermer un channel que depuis le côté de l'expéditeur, et seulement si cet expéditeur est le seul ou le dernier expéditeur actif. La fermeture par le récepteur ou lorsqu'il y a plusieurs expéditeurs peut entraîner des panics ou des comportements imprévisibles.
Pour gérer la fermeture avec plusieurs expéditeurs, une approche courante consiste à utiliser un channel de signalisation séparé ou sync.WaitGroup pour coordonner les expéditeurs, et un gestionnaire dédié pour fermer le channel principal une fois que tous les expéditeurs ont terminé.
Defer, Panic et Recover
defer
Le mot-clé defer permet de programmer l'exécution d'une fonction jusqu'à ce que la fonction englobante se termine. Les fonctions deferred sont exécutées dans l'ordre LIFO (Last-In, First-Out). Elles s'exécutent après le return de la fonction englobante, mais avant que la valeur de retour ne soit effectivement renvoyée à l'appelant, ce qui permet à une fonction deferred de modifier les valeurs de retour nommées.
Cas d'utilisation courants :
- Libération des ressources (fermeture de fichiers, déverrouillage de mutex).
- Récupération après un
panicavecrecover.
panic et recover
panic est utilisé pour indiquer une erreur irrécupérable qui arrête l'exécution normale d'un programme. Quand un panic se produit, les fonctions deferred sont exécutées séquentiellement jusqu'à atteindre la fin du programme ou jusqu'à ce qu'une fonction recover soit appelée dans une fonction deferred.
recover est une fonction intégrée qui permet de récupérer le contrôle d'une goroutine qui panique. Elle ne doit être appelée que dans une fonction deferred. Si recover est appelée en dehors d'une fonction deferred, elle retourne nil et n'a aucun effet.
Slices, Tableaux et Chaînes de Caractères
Slices
Une slice Go est une vue dynamique sur un tableau sous-jacent. Sa structure est composée de trois éléments :
type SliceHeader struct {
Data uintptr // Pointeur vers le premier élément du tableau sous-jacent
Len int // Longueur de la slice (nombre d'éléments accessibles)
Cap int // Capacité de la slice (nombre d'éléments disponibles à partir du pointeur Data)
}
Les slices sont des types de référence. Une slice nil (par exemple, var s []int) peut être utilisée avec append. Une slice vide (par exemple, s := []int{} ou s := make([]int, 0)) a une longueur et une capacité de 0 mais n'est pas nil.
Stratégie d'Agrandissement (Resizing)
Lorsqu'une slice n'a plus de capacité suffisante pour un append, Go alloue un nouveau tableau sous-jacent plus grand et copie les éléments. La stratégie d'agrandissement est la suivante :
- Si la capacité actuelle est inférieure à 1024 éléments, la capacité est doublée.
- Si la capacité actuelle est supérieure ou égale à 1024 éléments, la capacité augmente d'environ 25% (un facteur de 1.25).
- Si la capacité nécessaire est supérieure à l'agrandissement calculé, la capacité devient la capacité nécessaire.
Les slices ne sont pas thread-safe par défaut ; les accès concurrents nécessitent des mécanismes de synchronisation.
Tableaux (Arrays)
Les tableaux Go sont des structures de données de taille fixe dont la longueur fait partie de leur type (par exemple, [5]int est un type différent de [10]int). Ils sont passés par valeur. Leur avantage principal est la sécurité à la compilation et une préallocation de mémoire prévisible. Ils peuvent être comparés directement si leurs éléments sont comparables.
Chaînes de Caractères (Strings)
En Go, une chaîne de caractères est une séquence immuable d'octets. Elle peut représenter n'importe quelle donnée binaire, mais est généralement interprétée comme du texte UTF-8. La conversion entre string et []byte implique une copie des données, sauf si unsafe est utilisé pour des optimisations extrêmes, ce qui introduit des risques pour la sécurité mémoire et la compatibilité avec le GC.
Maps
Les maps en Go sont des collections non ordonnées de paires clé-valeur. Le type de la clé doit être comparable (bool, nombres, chaînes, pointeurs, channels, interfaces, structs et tableaux de types comparables). Les slices, maps et fonctions ne sont pas comparables et ne peuvent pas être des clés de map.
Utilisation et Erreurs Courantes
- Non initialisation : Une map déclarée avec
var m map[string]intestnilet ne peut pas être utilisée pour ajouter des éléments. Elle doit être initialisée avecmake(map[string]int). Tenter d'accéder à une clé dans une mapnilne paniquera pas mais retournera la valeur zéro du type de valeur. - Accès concurrents : Les maps intégrées ne sont pas thread-safe. Des accès concurrents en lecture/écriture entraîneront des panics. La map
sync.Mapdu packagesyncest conçue pour des scénarios de concurrence spécifiques, ou l'utilisation de mutex protégeant une map régulière est nécessaire.
Hashing et Résolution de Conflits
Go utilise le chaînage séparé pour résoudre les conflits de hachage. Chaque "seau" (bucket) d'une map est une petite structure qui peut contenir plusieurs paires clé-valeur. Si plusieurs clés hachent au même seau, elles sont stockées consécutivement dans ce seau. Quand un seau est plein, un nouveau seau est alloué et lié au précédent.
Ordre de Parcours
L'ordre de parcours des maps est arbitraire et non déterministe. Go randomise l'ordre à chaque itération pour éviter que les développeurs ne se fient à un ordre particulier et pour exposer les bugs potentiels dus à une dépendance d'ordre.
Interfaces et Réflexion
Interfaces
Les interfaces en Go sont des types qui définissent un ensemble de signatures de méthodes. Un type implémente une interface s'il fournit toutes les méthodes de l'interface. L'implémentation est implicite, sans mot-clé explicite.
En interne, une interface est représentée par deux mots machine :
- eface (pour
interface{}) : Contient un pointeur vers les données et un pointeur vers un_type(informations sur le type concret). - itab (pour les interfaces non vides) : Contient un pointeur vers un
_typeet un pointeur vers une table de méthodes (itab), qui mappe les méthodes de l'interface aux méthodes du type concret.
Une distinction importante est qu'une interface peut être non nil même si sa valeur interne est nil. Par exemple, si une fonction retourne une interface, et que le type concret assigné à cette interface est un pointeur nil, l'interface elle-même ne sera pas nil car son pointeur de type sera défini.
Réflexion
Le package reflect permet d'inspecter et de modifier la structure d'un programme Go à l'exécution. Il fournit des fonctions pour obtenir le Type (informations sur le type) et la Value (valeur concrète) de n'importe quelle variable. La réflexion est puissante mais doit être utilisée avec parcimonie car elle peut être plus lente et moins sûre en termes de types que le code statiquement typé.
Contexte (Context)
Le package context fournit un moyen standard de propager des informations spécifiques à une requête, des délais, des signaux d'annulation et d'autres valeurs au-delà des limites d'API entre les processus. C'est un outil essentiel pour la gestion des goroutines dans des applications complexes.
Fonctionnalités Clés :
- Annulation : Un
Contextpeut être annulé, signalant à toutes les goroutines qui écoutent ceContext(ou un de ses enfants) qu'elles doivent cesser leur travail. Utile pour les timeouts ou l'arrêt gracieux d'une chaîne d'opérations. - Délais : Un
Contextpeut être configuré avec une date limite (WithDeadline) ou une durée (WithTimeout), après laquelle il est automatiquement annulé. - Valeurs : Des valeurs arbitraires peuvent être associées à un
Context(WithValue) et récupérées par n'importe quelle goroutine en aval.
Un Context parent annulé entraîne l'annulation automatique de tous ses Context enfants. Ceci permet une gestion hiérarchique et en cascade des opérations concurrentes.
Primitives de Concurrence (Package sync)
Le package sync de Go fournit des primitives de bas niveau pour la synchronisation, essentielles pour les scénarios où les canaux ne sont pas adaptés ou pour optimiser les performances.
sync.Mutex (Verrou Mutex)
Un Mutex est un verrou d'exclusion mutuelle, permettant à une seule goroutine d'accéder à une ressource partagée à la fois. Go Mutex implémente un algorithme hybride "normal" et "affamé" (starving). En mode normal, les goroutines sont débloquées dans l'ordre FIFO (First-In, First-Out). En mode affamé, une goroutine qui attend le plus longtemps a la priorité pour obtenir le verrou.
Erreurs courantes :
- Verrouillage/déverrouillage non apparié.
- Copie d'un
Mutexdéjà utilisé. - Mutex non réentrant (tenter de verrouiller un
Mutexdéjà détenu par la même goroutine conduit à un interblocage).
// Implémentation d'un Mutex récursif utilisant un identifiant de goroutine (potentiellement non portable)
import (
"fmt"
"sync"
"sync/atomic"
)
// RecursiveLock implémente un verrou récursif
type RecursiveLock struct {
sync.Mutex
proprietaire int64 // ID de la goroutine qui détient le verrou
recursions int32 // Nombre de récursions
}
// Lock acquiert le verrou. Si la goroutine actuelle le détient déjà, incrémente le compteur de récursions.
func (m *RecursiveLock) Lock() {
// Cette partie nécessite une fonction pour obtenir un GID.
// Dans un vrai environnement, obtenir un GID stable et sûr est complexe et non portable.
// Pour l'exemple, nous allons simuler un GID simple.
// Pour un usage réel, github.com/petermattis/goid pourrait être utilisé,
// mais il est considéré comme une béquille et non recommandé pour la production.
currentGoroutineID := getGoroutineID() // Fonction hypothétique
if atomic.LoadInt64(&m.proprietaire) == currentGoroutineID {
m.recursions++
return
}
m.Mutex.Lock()
atomic.StoreInt64(&m.proprietaire, currentGoroutineID)
m.recursions = 1
}
// Unlock libère le verrou. Décrémente le compteur de récursions.
// Le verrou n'est complètement libéré que lorsque le compteur de récursions atteint zéro.
func (m *RecursiveLock) Unlock() {
currentGoroutineID := getGoroutineID() // Fonction hypothétique
if atomic.LoadInt64(&m.proprietaire) != currentGoroutineID {
panic(fmt.Sprintf("Verrou détenu par %d, tentative de déverrouillage par %d!", m.proprietaire, currentGoroutineID))
}
m.recursions--
if m.recursions != 0 {
return
}
atomic.StoreInt64(&m.proprietaire, 0) // Aucun propriétaire
m.Mutex.Unlock()
}
// getGoroutineID est une fonction de simulation pour l'exemple.
// Dans la réalité, obtenir un ID de goroutine est complexe et non officiel.
var nextGoroutineID int64
func getGoroutineID() int64 {
// Dans un environnement réel, cela pourrait être un identifiant unique pour chaque goroutine
// ou utiliser des packages non officiels.
// Pour simplifier l'exemple, nous allons juste retourner 1 pour simuler une seule goroutine
// qui tenterait d'appeler de manière récursive.
// Pour un test réel de récursivité, chaque goroutine devrait avoir un ID unique.
return 1
}
sync.RWMutex (Verrou de Lecture/Écriture)
Un RWMutex permet à plusieurs lecteurs d'accéder concurremment à une ressource, mais un seul écrivain à la fois. De plus, un écrivain bloque tous les lecteurs pendant son accès. La conception du RWMutex de Go est "Write-preferring" (préférence à l'écriture), ce qui signifie qu'un appel Lock en attente bloquera les nouvelles requêtes de lecture, évitant ainsi la famine des écrivains.
sync.Once (Exécution Unique)
sync.Once garantit qu'une fonction sera exécutée exactement une fois, quelle que soit la concurrence. C'est idéal pour l'initialisation de singletons ou d'autres configurations critiques.
import (
"fmt"
"sync"
"sync/atomic"
)
// InitialisateurUnique encapsule sync.Once pour une initialisation flexible.
type InitialisateurUnique struct {
mu sync.Mutex
fait uint32
}
// Executer garantit que la fonction f ne sera appelée qu'une seule fois.
// Si f retourne une erreur, l'état de l'initialisateur n'est pas marqué comme "fait".
func (o *InitialisateurUnique) Executer(f func() error) error {
if atomic.LoadUint32(&o.fait) == 1 { // Chemin rapide
return nil
}
o.mu.Lock()
defer o.mu.Unlock()
var err error
if o.fait == 0 { // Double vérification
err = f()
if err == nil {
atomic.StoreUint32(&o.fait, 1) // Marquer comme fait uniquement si succès
}
}
return err
}
var initialisationGlobale InitialisateurUnique
var ressource string
func initialiserRessource() error {
fmt.Println("Initialisation de la ressource...")
// Simuler un travail d'initialisation
ressource = "Ressource Initialisée !"
return nil
}
func main() {
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
err := initialisationGlobale.Executer(initialiserRessource)
if err != nil {
fmt.Printf("Goroutine %d: Erreur d'initialisation: %v\n", id, err)
} else {
fmt.Printf("Goroutine %d: Ressource: %s\n", id, ressource)
}
}(i)
}
wg.Wait()
}
sync.WaitGroup (Groupe d'Attente)
Un WaitGroup attend la fin d'un ensemble de goroutines. Il fonctionne comme un compteur :
Add(delta int): Incrémente ou décrémente le compteur.Done(): Décrémente le compteur de 1 (équivalent àAdd(-1)).Wait(): Bloque jusqu'à ce que le compteur atteigne zéro.
sync.Pool (Pool de Tampons)
sync.Pool fournit un pool d'objets temporaires qui peuvent être réutilisés, réduisant ainsi la pression sur le ramasse-miettes et les allocations coûteuses. Les objets dans le pool peuvent être supprimés par le GC à tout moment.
sync/atomic (Opérations Atomiques)
Le package sync/atomic fournit des opérations atomiques de bas niveau pour des types entiers et des pointeurs, permettant des mises à jour sûres sans verrous pour des opérations simples comme l'incrémentation, la décrémentation, la comparaison et l'échange (CAS).
Performance et Surveillance
Go offre des outils robustes pour l'analyse des performances et la détection des problèmes :
pprof: Un profileur de performance intégré qui peut collecter des profils CPU, mémoire (heap), blocage de mutex, goroutines et traces. Il est essentiel pour identifier les "points chauds" du code. Les flammes graphiques (flame graphs) générées parpprofsont très utiles pour visualiser l'utilisation du CPU et des fonctions appelées.trace: Génère des traces d'exécution détaillées de l'activité du runtime Go (événements du planificateur, GC, E/S).race: Le détecteur de data race identifie les accès concurrents non synchronisés aux données partagées, qui peuvent conduire à des bugs difficiles à reproduire. Il est activé avec l'option-racelors de la compilation ou de l'exécution (go run -race,go test -race).dlv(Delve) : Un débogueur pour Go, permettant de définir des points d'arrêt, d'inspecter les variables, et de suivre l'exécution du programme.