Menu
Coddy logo textTech

Progettazione di struct thread-safe

Fa parte della sezione Programmazione orientata agli oggetti del percorso GO di Coddy. Lezione 65 di 107.

Ora che hai capito i mutex e i WaitGroup, combiniamoli per progettare struct sicure da usare contemporaneamente da più goroutine. Una struct thread-safe racchiude la sincronizzazione nei suoi metodi, così chi la usa non deve preoccuparsi dei lock.

Lo schema è semplice: inserisci un mutex nella tua struct e acquisisci il lock in ogni metodo che accede allo stato condiviso:

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
}

Nota che anche il metodo di sola lettura Value() blocca il mutex. Senza questo, una goroutine potrebbe leggere mentre un’altra scrive, causando una race condition. Se le letture sono molto più frequenti delle scritture, usa invece sync.RWMutex e chiama RLock() per le letture.

Un principio fondamentale di progettazione: mantieni privato il mutex. Usando un nome di campo in minuscolo (mu), impedisci al codice esterno di accedervi direttamente. Tutta la sincronizzazione avviene tramite i tuoi metodi, dandoti il pieno controllo sulla sicurezza dei thread.

Per le struct con più campi, proteggi tutti i campi correlati con lo stesso mutex per garantire uno stato coerente:

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

Sia balance che history vengono aggiornati atomicamente: nessuna goroutine può osservare uno stato incoerente in cui uno è cambiato ma l’altro no.

challenge icon

Sfida

Facile

Costruiamo un sistema di conto bancario sicuro per l’uso concorrente, che dimostri il corretto incapsulamento della sincronizzazione nei metodi della struct. Il conto gestirà in sicurezza depositi, prelievi e controlli del saldo concorrenti, senza esporre ai chiamanti i dettagli dei lock.

Organizzerai il codice in due file:

  • account.go: definisci il tuo conto bancario sicuro per l’uso concorrente.

    Crea una struct BankAccount con un sync.Mutex incorporato, un campo balance (int) e una slice transactions che registra come stringhe tutte le operazioni riuscite.

    Implementa questi metodi:

    • NewBankAccount(initial int) *BankAccount - Crea un nuovo conto con il saldo iniziale specificato e una slice delle transazioni vuota
    • Deposit(amount int) - Aggiunge l’importo al saldo e registra la transazione come +[amount]
    • Withdraw(amount int) bool - Se i fondi sono sufficienti, sottrae l’importo, registra -[amount] e restituisce true. Altrimenti restituisce false senza modificare nulla
    • Balance() int - Restituisce il saldo corrente
    • History() []string - Restituisce una copia della slice delle transazioni

    Ogni metodo che accede ai campi della struct deve bloccare il mutex per garantire la sicurezza per l’uso concorrente. Usa defer per sbloccarlo. Mantieni il mutex e tutti i campi non esportati (in minuscolo), in modo che il codice esterno debba usare i tuoi metodi.

  • main.go: elabora le operazioni bancarie e mostra come usare il tuo conto sicuro per l’uso concorrente.

    Leggi il saldo iniziale e poi il numero di operazioni. Per ogni operazione, leggi il tipo (deposit, withdraw o balance) e, per depositi/prelievi, leggi l’importo.

    Stampa il risultato di ogni operazione:

    • deposit: stampa Deposited [amount], Balance: [new balance]
    • withdraw: se l’operazione riesce, stampa Withdrew [amount], Balance: [new balance]; altrimenti stampa Withdrawal failed: insufficient funds
    • balance: stampa Current balance: [balance]

    Dopo tutte le operazioni, stampa la cronologia delle transazioni, con ogni voce su una nuova riga e il prefisso History: solo per la prima voce.

Verranno forniti i seguenti input:

  • Riga 1: saldo iniziale (intero)
  • Riga 2: numero di operazioni (intero)
  • Righe successive: per ogni operazione, il tipo (deposit, withdraw o balance) e, per depositi/prelievi, l’importo sulla riga successiva

Per esempio, dato:

100
5
deposit
50
balance
withdraw
30
withdraw
200
balance

Il tuo output dovrebbe essere:

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

Il principio fondamentale è che tutta la sincronizzazione è nascosta all’interno dei metodi di BankAccount. I chiamanti usano semplicemente Deposit(), Withdraw() e Balance(), senza mai doversi preoccupare dei lock. La tua struct gestisce internamente la sicurezza per l’uso concorrente.

Provalo tu

package main

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

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

	// Leggi il saldo iniziale
	initialStr, _ := reader.ReadString('\n')
	initial, _ := strconv.Atoi(strings.TrimSpace(initialStr))

	// Leggi il numero di operazioni
	numOpsStr, _ := reader.ReadString('\n')
	numOps, _ := strconv.Atoi(strings.TrimSpace(numOpsStr))

	// Crea il conto bancario
	account := NewBankAccount(initial)

	// Elabora ogni operazione
	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: Chiama Deposit e stampa il risultato
			// Formato: "Deposited [amount], Balance: [new balance]"

		case "withdraw":
			amountStr, _ := reader.ReadString('\n')
			amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
			// TODO: Chiama Withdraw e stampa il risultato appropriato
			// Se riesce: "Withdrew [amount], Balance: [new balance]"
			// Se fallisce: "Withdrawal failed: insufficient funds"
			_ = amount // Rimuovi questa riga quando implementi la soluzione

		case "balance":
			// TODO: Chiama Balance e stampa il risultato
			// Formato: "Current balance: [balance]"
		}
	}

	// TODO: Stampa la cronologia delle transazioni
	// La prima voce deve essere preceduta da "History: "
	// Le voci successive devono essere su nuove righe senza prefisso
}
quiz iconMettiti alla prova

Questa lezione include un breve quiz. Inizia la lezione per rispondere e tenere traccia dei tuoi progressi.

Tutte le lezioni di Programmazione orientata agli oggetti

Esercitati da solo: Compilatore Go online