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 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.

challenge icon

Défi

Facile

Construisons 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 BankAccount contenant un sync.Mutex intégré, un champ balance (int) et une tranche transactions qui 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 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 renvoie true. Sinon, renvoie false sans rien modifier
    • Balance() int - Renvoie le solde actuel
    • History() []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 defer pour 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, withdraw ou balance) et, pour un dépôt ou un retrait, lisez le montant.

    Affichez les résultats de chaque opération :

    • deposit : Affichez Deposited [amount], Balance: [new balance]
    • withdraw : Affichez Withdrew [amount], Balance: [new balance] en cas de réussite, ou Withdrawal failed: insufficient funds dans le cas contraire
    • balance : Affichez Current 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, withdraw ou balance) 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
balance

Votre sortie doit être :

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

Le 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
}
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

Entraînez-vous par vous-même : Compilateur Go en ligne