Menu
Coddy logo textTech

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.

challenge icon

Défi

Facile

Construisons 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 BankAccount avec un sync.Mutex intégré, un champ balance (int) et une tranche (slice) transactions qui 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 vide
    • Deposit(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 retourne true. Sinon, retourne false sans rien modifier
    • Balance() int - Retourne le solde actuel
    • History() []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 defer pour 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, withdraw ou balance) et pour deposit/withdraw, lisez le montant.

    Affichez les résultats pour chaque opération :

    • deposit : Affichez Deposited [amount], Balance: [new balance]
    • withdraw : Affichez Withdrew [amount], Balance: [new balance] en cas de succès, ou Withdrawal failed: insufficient funds sinon
    • balance : Affichez Current 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, withdraw ou balance), et pour deposit/withdraw, le montant sur la ligne suivante

Par exemple, étant donné :

100
5
deposit
50
balance
withdraw
30
withdraw
200
balance

Votre sortie devrait être :

Deposited 50, Balance: 150
Current balance: 150
Withdrew 30, Balance: 120
Withdrawal failed: insufficient funds
Current balance: 120
History: +50
-30

Le 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 defer pour 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.RWMutex et appelez RLock() 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
}
quiz iconTestez-vous

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