Menu
Coddy logo textTech

Projektowanie struktur bezpiecznych wątkowo

Część sekcji Programowanie obiektowe ścieżki GO w Coddy. Lekcja 65 z 107.

Skoro znasz już mutexy i WaitGroups, połączmy je, aby zaprojektować struktury, których można bezpiecznie używać jednocześnie z wielu goroutines. Struktura bezpieczna wątkowo enkapsuluje synchronizację w swoich metodach, dzięki czemu wywołujący nie muszą martwić się blokowaniem.

Wzorzec jest prosty: umieść mutex w swojej strukturze i blokuj go w każdej metodzie, która uzyskuje dostęp do współdzielonego stanu:

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
}

Zauważ, że nawet tylko do odczytu metoda Value() blokuje muteks. Bez tego jedna gorutyna mogłaby odczytywać, gdy inna zapisuje, powodując wyścig danych. Jeśli odczyty są znacznie częstsze niż zapisy, użyj zamiast tego sync.RWMutex i wywołuj RLock() przy odczytach.

Kluczowa zasada projektowania: zachowaj mutex jako prywatny. Używając nazwy pola pisanej małymi literami (mu), uniemożliwiasz zewnętrznemu kodowi bezpośredni dostęp do niego. Cała synchronizacja odbywa się za pośrednictwem Twoich metod, co daje Ci pełną kontrolę nad bezpieczeństwem wątków.

W przypadku struktur z wieloma polami chroń wszystkie powiązane pola za pomocą tego samego mutexu, aby zapewnić spójny stan:

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

Zarówno balance, jak i history są aktualizowane atomowo: żadna gorutyna nie może zaobserwować niespójnego stanu, w którym jedno uległo zmianie, a drugie nie.

challenge icon

Wyzwanie

Łatwy

Zbudujmy bezpieczny wątkowo system konta bankowego, który pokazuje prawidłowe hermetyzowanie synchronizacji w metodach struktury. Twoje konto będzie bezpiecznie obsługiwać współbieżne wpłaty, wypłaty i sprawdzanie salda, nie ujawniając wywołującym żadnych szczegółów dotyczących blokad.

Rozmieścisz kod w dwóch plikach:

  • account.go: Zdefiniuj bezpieczne wątkowo konto bankowe.

    Utwórz strukturę BankAccount z osadzonym sync.Mutex, polem balance (int) oraz wycinkiem transactions, który zapisuje wszystkie udane operacje jako ciągi znaków.

    Zaimplementuj następujące metody:

    • NewBankAccount(initial int) *BankAccount - Tworzy nowe konto z podanym saldem początkowym i pustym wycinkiem transakcji
    • Deposit(amount int) - Dodaje kwotę do salda i zapisuje transakcję jako +[amount]
    • Withdraw(amount int) bool - Jeśli dostępne są wystarczające środki, odejmuje kwotę, zapisuje -[amount] i zwraca true. W przeciwnym razie zwraca false i niczego nie zmienia
    • Balance() int - Zwraca bieżące saldo
    • History() []string - Zwraca kopię wycinka transakcji

    Każda metoda uzyskująca dostęp do pól struktury musi zablokować mutex, aby zapewnić bezpieczeństwo wątkowe. Do odblokowania użyj defer. Zostaw mutex i wszystkie pola nieeksportowane (pisane małą literą), aby kod zewnętrzny musiał korzystać z metod.

  • main.go: Przetwarzaj operacje bankowe i zaprezentuj bezpieczne wątkowo konto.

    Wczytaj saldo początkowe, a następnie liczbę operacji. Dla każdej operacji wczytaj jej typ (deposit, withdraw lub balance), a w przypadku wpłaty lub wypłaty wczytaj kwotę.

    Wypisz wyniki każdej operacji:

    • deposit: Wypisz Deposited [amount], Balance: [new balance]
    • withdraw: Jeśli operacja się powiedzie, wypisz Withdrew [amount], Balance: [new balance], a jeśli nie — Withdrawal failed: insufficient funds
    • balance: Wypisz Current balance: [balance]

    Po wykonaniu wszystkich operacji wypisz historię transakcji, umieszczając każdy wpis w nowym wierszu i dodając przed pierwszym wpisem prefiks History: .

Podane zostaną następujące dane wejściowe:

  • Wiersz 1: Saldo początkowe (liczba całkowita)
  • Wiersz 2: Liczba operacji (liczba całkowita)
  • Kolejne wiersze: Dla każdej operacji jej typ (deposit, withdraw lub balance), a w przypadku wpłaty lub wypłaty kwota w następnym wierszu

Na przykład dla danych wejściowych:

100
5
deposit
50
balance
withdraw
30
withdraw
200
balance

Wynik powinien wyglądać następująco:

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

Kluczowa zasada jest taka, że cała synchronizacja jest ukryta wewnątrz metod BankAccount. Wywołujący po prostu korzystają z metod Deposit(), Withdraw() i Balance(), nie myśląc o blokadach. Twoja struktura sama wewnętrznie zapewnia bezpieczeństwo wątkowe.

Spróbuj swoich sił

package main

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

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

	// Wczytaj saldo początkowe
	initialStr, _ := reader.ReadString('\n')
	initial, _ := strconv.Atoi(strings.TrimSpace(initialStr))

	// Wczytaj liczbę operacji
	numOpsStr, _ := reader.ReadString('\n')
	numOps, _ := strconv.Atoi(strings.TrimSpace(numOpsStr))

	// Utwórz konto bankowe
	account := NewBankAccount(initial)

	// Przetwórz każdą operację
	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: Wywołaj Deposit i wypisz wynik
			// Format: "Deposited [amount], Balance: [new balance]"

		case "withdraw":
			amountStr, _ := reader.ReadString('\n')
			amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
			// TODO: Wywołaj Withdraw i wypisz odpowiedni wynik
			// Jeśli operacja się powiodła: "Withdrew [amount], Balance: [new balance]"
			// Jeśli operacja się nie powiodła: "Withdrawal failed: insufficient funds"
			_ = amount // Usuń ten wiersz po zaimplementowaniu rozwiązania

		case "balance":
			// TODO: Wywołaj Balance i wypisz wynik
			// Format: "Current balance: [balance]"
		}
	}

	// TODO: Wypisz historię transakcji
	// Pierwszy wpis powinien być poprzedzony "History: "
	// Kolejne wpisy powinny znajdować się w osobnych wierszach, bez prefiksu
}
quiz iconSprawdź się

Ta lekcja zawiera krótki quiz. Zacznij lekcję, żeby na niego odpowiedzieć i śledzić swoje postępy.

Wszystkie lekcje w sekcji Programowanie obiektowe

Poćwicz samodzielnie: Kompilator Go online