Menu
Coddy logo textTech

Thread-sicheres Struct-Design

Teil des Abschnitts Objektorientierte Programmierung der GO-Journey von Coddy — Lektion 65 von 107.

Nachdem Sie nun Mutexe und WaitGroups verstehen, lassen Sie uns diese kombinieren, um Structs zu entwerfen, die sicher von mehreren Goroutines gleichzeitig verwendet werden können. Eine threadsichere Struct kapselt die Synchronisierung innerhalb ihrer Methoden, sodass sich die Aufrufer nicht um das Sperren (Locking) kümmern müssen.

Das Muster ist einfach: Betten Sie einen Mutex in Ihr Struct ein und sperren Sie ihn in jeder Methode, die auf den gemeinsamen Zustand zugreift:

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
}

Beachten Sie, dass selbst die schreibgeschützte Value()-Methode den Mutex sperrt. Ohne dies könnte eine Goroutine lesen, während eine andere schreibt, was zu einer Data Race führt. Wenn Lesevorgänge weitaus häufiger sind als Schreibvorgänge, verwenden Sie stattdessen sync.RWMutex und rufen Sie RLock() für Lesevorgänge auf.

Ein wichtiges Designprinzip: den Mutex privat halten. Durch die Verwendung eines kleingeschriebenen Feldnamens (mu) verhindern Sie, dass externer Code direkt darauf zugreift. Die gesamte Synchronisierung erfolgt über Ihre Methoden, was Ihnen die volle Kontrolle über die Threadsicherheit gibt.

Schützen Sie bei Structs mit mehreren Feldern alle zusammengehörigen Felder mit demselben Mutex, um einen konsistenten Zustand zu gewährleisten:

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

Sowohl balance als auch history werden atomar aktualisiert – keine Goroutine kann einen inkonsistenten Zustand beobachten, in dem sich das eine geändert hat, das andere jedoch nicht.

challenge icon

Aufgabe

Einfach

Lassen Sie uns ein threadsicheres Bankkontosystem bauen, das die korrekte Kapselung der Synchronisation innerhalb von Struct-Methoden demonstriert. Ihr Konto wird gleichzeitige Einzahlungen, Auszahlungen und Kontostandsabfragen sicher verarbeiten, ohne den Aufrufern Details zur Sperrung (Locking) offenzulegen.

Sie werden Ihren Code auf zwei Dateien aufteilen:

  • account.go: Definieren Sie Ihr threadsicheres Bankkonto.

    Erstellen Sie ein BankAccount-Struct mit einem eingebetteten sync.Mutex, einem balance-Feld (int) und einem transactions-Slice, das alle erfolgreichen Operationen als Strings aufzeichnet.

    Implementieren Sie diese Methoden:

    • NewBankAccount(initial int) *BankAccount - Erstellt ein neues Konto mit dem angegebenen Anfangsguthaben und einem leeren Transaktions-Slice
    • Deposit(amount int) - Fügt den Betrag zum Guthaben hinzu und zeichnet die Transaktion als +[amount] auf
    • Withdraw(amount int) bool - Wenn ausreichend Guthaben vorhanden ist, zieht es den Betrag ab, zeichnet -[amount] auf und gibt true zurück. Andernfalls wird false zurückgegeben, ohne etwas zu ändern
    • Balance() int - Gibt den aktuellen Kontostand zurück
    • History() []string - Gibt eine Kopie des Transaktions-Slices zurück

    Jede Methode, die auf die Felder des Structs zugreift, muss den Mutex sperren, um Threadsicherheit zu gewährleisten. Verwenden Sie defer zum Entsperren. Halten Sie den Mutex und alle Felder unexportiert (kleingeschrieben), sodass externer Code Ihre Methoden verwenden muss.

  • main.go: Verarbeiten Sie Bankoperationen und demonstrieren Sie Ihr threadsicheres Konto.

    Lesen Sie das Anfangsguthaben und dann die Anzahl der Operationen ein. Lesen Sie für jede Operation den Typ (deposit, withdraw oder balance) und für deposit/withdraw den Betrag ein.

    Geben Sie die Ergebnisse für jede Operation aus:

    • deposit: Geben Sie Deposited [amount], Balance: [new balance] aus
    • withdraw: Geben Sie Withdrew [amount], Balance: [new balance] aus, wenn erfolgreich, oder Withdrawal failed: insufficient funds, wenn nicht
    • balance: Geben Sie Current balance: [balance] aus

    Geben Sie nach allen Operationen den Transaktionsverlauf aus, wobei jeder Eintrag in einer neuen Zeile steht, vorangestellt von History: nur für den ersten Eintrag.

Die folgenden Eingaben werden bereitgestellt:

  • Zeile 1: Anfangsguthaben (Ganzzahl)
  • Zeile 2: Anzahl der Operationen (Ganzzahl)
  • Folgende Zeilen: Für jede Operation der Typ (deposit, withdraw oder balance) und für deposit/withdraw der Betrag in der nächsten Zeile

Zum Beispiel, gegeben:

100
5
deposit
50
balance
withdraw
30
withdraw
200
balance

Ihre Ausgabe sollte wie folgt aussehen:

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

Das Schlüsselprinzip hierbei ist, dass die gesamte Synchronisation innerhalb Ihrer BankAccount-Methoden verborgen ist. Aufrufer verwenden einfach Deposit(), Withdraw() und Balance(), ohne jemals über Sperren nachzudenken – Ihr Struct kümmert sich intern um die Threadsicherheit.

Spickzettel

Eine thread-sichere Struct kapselt die Synchronisierung innerhalb ihrer Methoden, indem sie einen Mutex einbettet und diesen in jeder Methode sperrt, die auf den gemeinsamen Zustand zugreift:

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
}

Wichtige Prinzipien:

  • Sperren Sie den Mutex in allen Methoden, die auf den gemeinsamen Zustand zugreifen, einschließlich schreibgeschützter Methoden, um Data Races zu verhindern
  • Verwenden Sie defer, um sicherzustellen, dass der Mutex entsperrt wird, selbst wenn die Funktion vorzeitig beendet wird
  • Halten Sie den Mutex privat (Feldname in Kleinschreibung), damit externer Code nicht direkt darauf zugreifen kann
  • Verwenden Sie bei leseintensiven Workloads sync.RWMutex und rufen Sie RLock() für Lesevorgänge auf

Schützen Sie bei Structs mit mehreren Feldern alle zusammengehörigen Felder mit demselben Mutex, um einen konsistenten Zustand zu gewährleisten:

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

Dies stellt sicher, dass sowohl balance als auch history atomar aktualisiert werden – keine Goroutine kann einen inkonsistenten Zustand beobachten.

Probier es selbst

package main

import (
	"bufio"
	"fmt"
	"os"
	"strconv"
	"strings"
)

func main() {
	reader := bufio.NewReader(os.Stdin)

	// Anfangssaldo lesen
	initialStr, _ := reader.ReadString('\n')
	initial, _ := strconv.Atoi(strings.TrimSpace(initialStr))

	// Anzahl der Operationen lesen
	numOpsStr, _ := reader.ReadString('\n')
	numOps, _ := strconv.Atoi(strings.TrimSpace(numOpsStr))

	// Das Bankkonto erstellen
	account := NewBankAccount(initial)

	// Jede Operation verarbeiten
	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: Deposit aufrufen und das Ergebnis ausgeben
			// Format: "Deposited [amount], Balance: [new balance]"

		case "withdraw":
			amountStr, _ := reader.ReadString('\n')
			amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
			// TODO: Withdraw aufrufen und das entsprechende Ergebnis ausgeben
			// Wenn erfolgreich: "Withdrew [amount], Balance: [new balance]"
			// Wenn fehlgeschlagen: "Withdrawal failed: insufficient funds"
			_ = amount // Diese Zeile bei der Implementierung entfernen

		case "balance":
			// TODO: Balance aufrufen und das Ergebnis ausgeben
			// Format: "Current balance: [balance]"
		}
	}

	// TODO: Transaktionsverlauf ausgeben
	// Der erste Eintrag sollte das Präfix "History: " haben
	// Nachfolgende Einträge sollten in neuen Zeilen ohne Präfix stehen
}
quiz iconTeste dich selbst

Diese Lektion enthält ein kurzes Quiz. Starte die Lektion, um es zu beantworten und deinen Fortschritt zu speichern.

Alle Lektionen in Objektorientierte Programmierung