Menu
Coddy logo textTech

Thread-sicheres Struct-Design

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

Da du nun Mutexes und WaitGroups verstehst, wollen wir sie kombinieren, um Structs zu entwerfen, die sicher von mehreren Goroutinen gleichzeitig verwendet werden können. Ein thread-sicheres Struct kapselt die Synchronisierung in seinen Methoden, sodass sich Aufrufer nicht um das Sperren kümmern müssen.

Das Muster ist unkompliziert: Bette einen Mutex in deine Struktur ein und sperre ihn in jeder Methode, die auf den gemeinsam genutzten 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
}

Beachte, dass sogar die schreibgeschützte Methode Value() den Mutex sperrt. Ohne dies könnte eine Goroutine lesen, während eine andere schreibt, was zu einer Data Race führen würde. Wenn Lesevorgänge weitaus häufiger als Schreibvorgänge sind, verwende stattdessen sync.RWMutex und rufe für Lesevorgänge RLock() auf.

Ein wichtiges Designprinzip: Halte den Mutex privat. Durch die Verwendung eines kleingeschriebenen Feldnamens (mu) verhinderst du, dass externer Code direkt darauf zugreift. Die gesamte Synchronisierung erfolgt über deine Methoden, sodass du die vollständige Kontrolle über die Thread-Sicherheit hast.

Bei Strukturen mit mehreren Feldern schützt du alle zusammengehörigen Felder mit demselben Mutex, um einen konsistenten Zustand sicherzustellen:

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 eines geändert hat, das andere jedoch nicht.

challenge icon

Aufgabe

Einfach

Erstellen wir ein threadsicheres Bankkontosystem, das die ordnungsgemäße Kapselung der Synchronisierung innerhalb von Struct-Methoden demonstriert. Dein Konto verarbeitet gleichzeitige Einzahlungen, Auszahlungen und Kontostandsabfragen sicher, ohne Aufrufern Details zur Sperrung offenzulegen.

Du organisierst deinen Code über zwei Dateien:

  • account.go: Definiere dein threadsicheres Bankkonto.

    Erstelle ein BankAccount-Struct mit einem eingebetteten sync.Mutex, einem balance-Feld (int) und einem transactions-Slice, das alle erfolgreichen Vorgänge als Strings aufzeichnet.

    Implementiere diese Methoden:

    • NewBankAccount(initial int) *BankAccount – Erstellt ein neues Konto mit dem angegebenen Anfangssaldo und einem leeren transactions-Slice
    • Deposit(amount int) – Addiert den Betrag zum Kontostand und zeichnet den Vorgang als +[amount] auf
    • Withdraw(amount int) bool – Wenn ausreichende Mittel vorhanden sind, zieht die Methode den Betrag ab, zeichnet -[amount] auf und gibt true zurück. Andernfalls gibt sie false zurück, ohne etwas zu ändern
    • Balance() int – Gibt den aktuellen Kontostand zurück
    • History() []string – Gibt eine Kopie des transactions-Slices zurück

    Jede Methode, die auf die Felder des Structs zugreift, muss den Mutex sperren, um Thread-Sicherheit zu gewährleisten. Verwende defer zum Entsperren. Halte den Mutex und alle Felder nicht exportiert (kleingeschrieben), damit externer Code deine Methoden verwenden muss.

  • main.go: Verarbeite Bankvorgänge und demonstriere dein threadsicheres Konto.

    Lies den Anfangssaldo und anschließend die Anzahl der Vorgänge ein. Lies für jeden Vorgang den Typ (deposit, withdraw oder balance) und bei deposit/withdraw den Betrag ein.

    Gib für jeden Vorgang Ergebnisse aus:

    • deposit: Gib Deposited [amount], Balance: [new balance] aus
    • withdraw: Gib bei Erfolg Withdrew [amount], Balance: [new balance] aus oder andernfalls Withdrawal failed: insufficient funds
    • balance: Gib Current balance: [balance] aus

    Gib nach allen Vorgängen den Transaktionsverlauf aus, wobei jeder Eintrag in einer neuen Zeile steht und nur dem ersten Eintrag History: vorangestellt wird.

Die folgenden Eingaben werden bereitgestellt:

  • Zeile 1: Anfangssaldo (Ganzzahl)
  • Zeile 2: Anzahl der Vorgänge (Ganzzahl)
  • Folgende Zeilen: Für jeden Vorgang der Typ (deposit, withdraw oder balance) und bei deposit/withdraw der Betrag in der nächsten Zeile

Beispiel für folgende Eingabe:

100
5
deposit
50
balance
withdraw
30
withdraw
200
balance

Deine Ausgabe sollte folgendermaßen aussehen:

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

Das wichtigste Prinzip ist hier, dass die gesamte Synchronisierung innerhalb deiner BankAccount-Methoden verborgen ist. Aufrufer verwenden einfach Deposit(), Withdraw() und Balance(), ohne sich jemals Gedanken über Sperren machen zu müssen. Dein Struct kümmert sich intern um die Thread-Sicherheit.

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
			// Bei Erfolg: "Withdrew [amount], Balance: [new balance]"
			// If failed: "Withdrawal failed: insufficient funds"
			_ = amount // Diese Zeile entfernen, wenn du implementierst

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

	// TODO: Transaktionsverlauf ausgeben
	// Der erste Eintrag sollte mit "History: " vorangestellt werden
	// 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

Übe selbstständig: Online-Go-Compiler