Conception de structures Thread-Safe
Fait partie de la section Programmation Orientée Objet du Journey GO de Coddy — leçon 65 sur 107.
Maintenant que vous comprenez les mutex et les WaitGroups, combinons-les pour concevoir des structures qui peuvent être utilisées en toute sécurité par plusieurs goroutines simultanément. Une structure thread-safe encapsule la synchronisation au sein de ses méthodes, de sorte que les appelants n'aient pas à se soucier du verrouillage.
Le modèle est simple : intégrez un mutex dans votre structure et verrouillez-le dans chaque méthode qui accède à l'état partagé :
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}Remarquez que même la méthode en lecture seule Value() verrouille le mutex. Sans cela, une goroutine pourrait lire pendant qu'une autre écrit, provoquant une situation de compétition (data race). Si les lectures sont beaucoup plus fréquentes que les écritures, utilisez sync.RWMutex à la place et appelez RLock() pour les lectures.
Un principe de conception clé : garder le mutex privé. En utilisant un nom de champ en minuscules (mu), vous empêchez le code externe d'y accéder directement. Toute la synchronisation se fait via vos méthodes, vous offrant un contrôle total sur la sécurité des threads.
Pour les structures comportant plusieurs champs, protégez tous les champs associés avec le même mutex afin de garantir un état cohérent :
type Account struct {
mu sync.Mutex
balance int
history []string
}
func (a *Account) Deposit(amount int) {
a.mu.Lock()
defer a.mu.Unlock()
a.balance += amount
a.history = append(a.history, fmt.Sprintf("+%d", amount))
}À la fois balance et history sont mis à jour de manière atomique — aucune goroutine ne peut observer un état incohérent où l'un a été modifié mais pas l'autre.
Défi
FacileConstruisons un système de compte bancaire thread-safe qui démontre une encapsulation appropriée de la synchronisation au sein des méthodes de structure. Votre compte gérera les dépôts, les retraits et les vérifications de solde simultanés en toute sécurité, sans exposer les détails de verrouillage aux appelants.
Vous organiserez votre code sur deux fichiers :
account.go: Définissez votre compte bancaire thread-safe.Créez une structure
BankAccountavec unsync.Mutexintégré, un champbalance(int) et une tranche (slice)transactionsqui enregistre toutes les opérations réussies sous forme de chaînes de caractères.Implémentez ces méthodes :
NewBankAccount(initial int) *BankAccount- Crée un nouveau compte avec le solde initial donné et une tranche de transactions videDeposit(amount int)- Ajoute le montant au solde et enregistre la transaction sous la forme+[amount]Withdraw(amount int) bool- Si les fonds sont suffisants, soustrait le montant, enregistre-[amount]et retournetrue. Sinon, retournefalsesans rien modifierBalance() int- Retourne le solde actuelHistory() []string- Retourne une copie de la tranche de transactions
Chaque méthode qui accède aux champs de la structure doit verrouiller le mutex pour garantir la sécurité des threads (thread safety). Utilisez
deferpour le déverrouillage. Gardez le mutex et tous les champs non exportés (minuscules) afin que le code externe doive utiliser vos méthodes.main.go: Traitez les opérations bancaires et démontrez votre compte thread-safe.Lisez le solde initial, puis le nombre d'opérations. Pour chaque opération, lisez le type (
deposit,withdrawoubalance) et pour deposit/withdraw, lisez le montant.Affichez les résultats pour chaque opération :
deposit: AffichezDeposited [amount], Balance: [new balance]withdraw: AffichezWithdrew [amount], Balance: [new balance]en cas de succès, ouWithdrawal failed: insufficient fundssinonbalance: AffichezCurrent balance: [balance]
Après toutes les opérations, affichez l'historique des transactions avec chaque entrée sur une nouvelle ligne, préfixée par
History:pour la première entrée uniquement.
Les entrées suivantes seront fournies :
- Ligne 1 : Solde initial (entier)
- Ligne 2 : Nombre d'opérations (entier)
- Lignes suivantes : Pour chaque opération, le type (
deposit,withdrawoubalance), et pour deposit/withdraw, le montant sur la ligne suivante
Par exemple, étant donné :
100
5
deposit
50
balance
withdraw
30
withdraw
200
balanceVotre sortie devrait être :
Deposited 50, Balance: 150
Current balance: 150
Withdrew 30, Balance: 120
Withdrawal failed: insufficient funds
Current balance: 120
History: +50
-30Le principe clé ici est que toute la synchronisation est cachée à l'intérieur de vos méthodes BankAccount. Les appelants utilisent simplement Deposit(), Withdraw() et Balance() sans jamais se soucier des verrous — votre structure gère la sécurité des threads en interne.
Aide-mémoire
Une structure thread-safe encapsule la synchronisation au sein de ses méthodes en intégrant un mutex et en le verrouillant dans chaque méthode qui accède à l'état partagé :
type SafeCounter struct {
mu sync.Mutex
count int
}
func (c *SafeCounter) Increment() {
c.mu.Lock()
defer c.mu.Unlock()
c.count++
}
func (c *SafeCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()
return c.count
}Principes clés :
- Verrouillez le mutex dans toutes les méthodes qui accèdent à l'état partagé, y compris les méthodes en lecture seule, pour éviter les situations de compétition (data races)
- Utilisez
deferpour garantir que le mutex est déverrouillé même si la fonction se termine prématurément - Gardez le mutex privé (nom de champ en minuscules) afin que le code externe ne puisse pas y accéder directement
- Pour les charges de travail lourdes en lecture, utilisez
sync.RWMutexet appelezRLock()pour les lectures
Pour les structures comportant plusieurs champs, protégez tous les champs associés avec le même mutex pour garantir un état cohérent :
type Account struct {
mu sync.Mutex
balance int
history []string
}
func (a *Account) Deposit(amount int) {
a.mu.Lock()
defer a.mu.Unlock()
a.balance += amount
a.history = append(a.history, fmt.Sprintf("+%d", amount))
}Cela garantit que balance et history sont mis à jour de manière atomique — aucune goroutine ne peut observer un état incohérent.
Essayez vous-même
package main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
reader := bufio.NewReader(os.Stdin)
// Lire le solde initial
initialStr, _ := reader.ReadString('\n')
initial, _ := strconv.Atoi(strings.TrimSpace(initialStr))
// Lire le nombre d'opérations
numOpsStr, _ := reader.ReadString('\n')
numOps, _ := strconv.Atoi(strings.TrimSpace(numOpsStr))
// Créer le compte bancaire
account := NewBankAccount(initial)
// Traiter chaque opération
for i := 0; i < numOps; i++ {
opType, _ := reader.ReadString('\n')
opType = strings.TrimSpace(opType)
switch opType {
case "deposit":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: Appeler Deposit et afficher le résultat
// Format: "Deposited [amount], Balance: [new balance]"
case "withdraw":
amountStr, _ := reader.ReadString('\n')
amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
// TODO: Appeler Withdraw et afficher le résultat approprié
// Si réussi: "Withdrew [amount], Balance: [new balance]"
// Si échec: "Withdrawal failed: insufficient funds"
_ = amount // Supprimer cette ligne lors de l'implémentation
case "balance":
// TODO: Appeler Balance et afficher le résultat
// Format: "Current balance: [balance]"
}
}
// TODO: Afficher l'historique des transactions
// La première entrée doit être préfixée par "History: "
// Les entrées suivantes doivent être sur de nouvelles lignes sans préfixe
}
Cette leçon comprend un petit quiz. Commencez la leçon pour y répondre et suivre votre progression.
Toutes les leçons de Programmation Orientée Objet
1Fondamentaux de la POO en Go
Fichiers externesEspace de travail et Modules GoPackages et ImportsNoms exportés vs non exportésIntroduction à la POO en GoLes Structs comme ClassesDéfinir des méthodes sur les StructsReceveurs par pointeur vs par valeurInitialisation de StructsFonctions constructeursRécapitulatif - Calculatrice simple4Interfaces
Introduction aux interfacesImplémentation impliciteL'interface comme contratInterface vide (any)Assertion de typeType SwitchComposition d'interfacesInterfaces Stringer & ErrorRécapitulatif - Calculateur de formes7Encapsulation
Champs exportés vs non exportésEncapsulation au niveau du packageMéthodes Getter & SetterMasquage d'informations en GoRécapitulatif - Dossiers étudiants10Génériques (Go 1.18+)
Introduction aux génériquesParamètres de typeContraintes de typeStructures génériquesContournement pour les méthodes génériquesRécapitulatif - Collections génériques13Patrons de conception, Partie 1
Introduction aux Design PatternsPatron SingletonPatron FactoryPatron Abstract FactoryPatron ObserverPatron Strategy2Exploration approfondie des Types et Structs
Types de base et compositesDéfinitions de types personnalisésTags de StructStructs anonymesStructs imbriquéesValeurs par défaut et Zero ValuesRécapitulatif - Carnet de contacts5Composition plutôt qu'héritage
Pourquoi Go n'a pas d'héritageLes bases de l'imbrication de structsPromotion de méthodesImbriquer plusieurs structsImbrication vs AgrégationMasquage de méthodes imbriquéesRécapitulatif - Hiérarchie des employés8Gestion des erreurs & POO
L'interface errorTypes d'erreurs personnalisésError Wrapping (fmt.Errorf)Erreurs sentinelleserrors.Is() et errors.As()Panic, Defer et RecoverRécapitulatif - File Parser11Bibliothèque standard et POO
io.Reader & io.WriterL'interface sort.InterfaceL'interface fmt.Stringerencoding/json avec les StructsL'interface http.HandlerRécapitulatif - Modèles d'API REST14Patrons de conception, Partie 2
Patron CommandePatron AdaptateurPatron DécorateurPatron Template MethodPatron ÉtatPatron CompositeMiddleware en tant que Décorateur3Pointeurs et Mémoire
Bases des pointeurs en GoPointeurs vers des structsPassage par valeur vs référenceLa fonction new()Garbage Collection en GoRécapitulatif - Constructeur de liste chaînée6Le polymorphisme en Go
Le polymorphisme via les interfacesLe Duck Typing en GoRègles de satisfaction des interfacesCollections polymorphesInjection de dépendancesRécapitulatif - Processeur de paiement9Concurrence et POO
Bases des GoroutinesChannels et CommunicationChannels avec buffer vs sans bufferL'instruction Selectsync.Mutex et sync.RWMutexsync.WaitGroupConception de structures Thread-SafeRécapitulatif - Worker Pool