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 tu comprends 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 dans ses méthodes, de sorte que les appelants n’ont 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
}Notez que même la méthode en lecture seule Value() verrouille le mutex. Sans cela, une goroutine pourrait lire pendant qu’une autre écrit, ce qui provoquerait une course aux données. Si les lectures sont beaucoup plus fréquentes que les écritures, utilisez plutôt sync.RWMutex et appelez RLock() pour les lectures.
Un principe de conception essentiel : gardez 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 s’effectue via vos méthodes, ce qui vous donne 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))
}balance et history sont mis à jour de manière atomique : aucune goroutine ne peut observer un état incohérent où l’un a changé, mais pas l’autre.
Défi
FacileConstruisons un système de compte bancaire sûr pour les threads, qui illustre l’encapsulation appropriée de la synchronisation au sein des méthodes d’une structure. Votre compte gérera de manière sûre les dépôts, les retraits et les vérifications de solde concurrents, sans exposer les détails du verrouillage aux appelants.
Vous organiserez votre code sur deux fichiers :
account.go: Définissez votre compte bancaire sûr pour les threads.Créez une structure
BankAccountcontenant unsync.Mutexintégré, un champbalance(int) et une tranchetransactionsqui enregistre toutes les opérations réussies sous forme de chaînes.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 renvoietrue. Sinon, renvoiefalsesans rien modifierBalance() int- Renvoie le solde actuelHistory() []string- Renvoie une copie de la tranche des transactions
Chaque méthode qui accède aux champs de la structure doit verrouiller le mutex afin de garantir la sécurité des threads. Utilisez
deferpour le déverrouillage. Gardez le mutex et tous les champs non exportés (en minuscules) afin que le code externe soit obligé d’utiliser vos méthodes.main.go: Traitez les opérations bancaires et illustrez votre compte sûr pour les threads.Lisez le solde initial, puis le nombre d’opérations. Pour chaque opération, lisez le type (
deposit,withdrawoubalance) et, pour un dépôt ou un retrait, lisez le montant.Affichez les résultats de chaque opération :
deposit: AffichezDeposited [amount], Balance: [new balance]withdraw: AffichezWithdrew [amount], Balance: [new balance]en cas de réussite, ouWithdrawal failed: insufficient fundsdans le cas contrairebalance: AffichezCurrent balance: [balance]
Après toutes les opérations, affichez l’historique des transactions, chaque entrée apparaissant sur une nouvelle ligne, avec le préfixe
History:uniquement pour la première entrée.
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 un dépôt ou un retrait, le montant à la ligne suivante
Par exemple, avec :
100
5
deposit
50
balance
withdraw
30
withdraw
200
balanceVotre sortie doit être :
Deposited 50, Balance: 150
Current balance: 150
Withdrew 30, Balance: 120
Withdrawal failed: insufficient funds
Current balance: 120
History: +50
-30Le principe essentiel est ici que toute la synchronisation est masquée à l’intérieur des méthodes de votre BankAccount. Les appelants utilisent simplement Deposit(), Withdraw() et Balance() sans jamais avoir à se préoccuper des verrous. Votre structure gère la sécurité des threads en interne.
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]"
// If failed: "Withdrawal failed: insufficient funds"
_ = amount // Supprimez cette ligne lorsque vous implémentez
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 et non exportésIntroduction à la POO en GoStructs comme classesDéfinir des méthodes sur des structsRécepteurs pointeurs ou par valeurInitialisation des structsFonctions constructeursRécapitulatif - Calculatrice simple4Interfaces
Introduction aux interfacesImplémentation impliciteL’interface comme contratInterface vide (any)Assertion de typeCommutation de typeComposition d’interfacesInterfaces Stringer et ErrorRécapitulatif - Calculateur de formes7Encapsulation
Champs exportés vs non exportésEncapsulation au niveau du packageMéthodes Getter et SetterDissimulation de l’information en GoRécapitulatif – Fiches d’étudiants10Génériques (Go 1.18+)
Introduction aux génériquesParamètres de typeContraintes de typeStructures génériquesSolution de contournement pour les méthodes génériquesRécapitulatif - Collection générique2Plongée approfondie dans les types et les structs
Types de base et compositesDéfinitions de types personnalisésTags de structStructs anonymesStructs imbriquésValeurs zéro et valeurs par défautRécapitulatif - Carnet de contacts5La composition plutôt que l’héritage
Pourquoi Go n’a pas d’héritageBases de l’inclusion de structsPromotion des méthodesInclure plusieurs structsInclusion ou agrégationMasquage des méthodes inclusesRécapitulatif : hiérarchie des employés8Gestion des erreurs et POO
L’interface errorTypes d’erreurs personnalisésEnrobage des erreurs (fmt.Errorf)Erreurs sentinelleserrors.Is() et errors.As()Panic, Defer et RecoverRécapitulatif - Analyseur de fichiers11Bibliothèque standard et POO
io.Reader et io.Writersort.InterfaceInterface fmt.Stringerencoding/json avec des structsInterface http.HandlerRécapitulatif - Modèles d’API REST14Modèles de conception – Partie 2
Modèle CommandeModèle AdaptateurModèle DécorateurModèle Méthode modèleModèle ÉtatModèle CompositeMiddleware comme décorateur3Pointeurs et mémoire
Notions de base des pointeurs en GoPointeurs vers des structuresPassage par valeur ou par référenceLa fonction new()Garbage collection en GoRécapitulatif - Constructeur de listes chaînées6Polymorphisme en Go
Polymorphisme via les interfacesDuck typing en GoRègles de satisfaction des interfacesCollections polymorphesInjection de dépendancesRécapitulatif – Processeur de paiements9Concurrence et POO
Bases des GoroutinesCanaux et communicationCanaux tamponnés ou non tamponnésInstruction selectsync.Mutex et sync.RWMutexsync.WaitGroupConception de structures thread-safeRécapitulatif - Pool de workersEntraînez-vous par vous-même : Compilateur Go en ligne