Menu
Coddy logo textTech

Diseño de estructuras Thread-Safe

Parte de la sección Programación Orientada a Objetos del Journey de GO de Coddy. Lección 65 de 107.

Ahora que entiendes los mutexes y los WaitGroups, combinémoslos para diseñar estructuras que sean seguras de usar simultáneamente desde varias gorutinas. Una estructura segura para la concurrencia encapsula la sincronización dentro de sus métodos, por lo que quienes la llaman no tienen que preocuparse por el bloqueo.

El patrón es sencillo: incorpora un mutex en tu estructura y bloquéalo en cada método que acceda al estado compartido:

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
}

Observa que incluso el método de solo lectura Value() bloquea el mutex. Sin esto, una gorutina podría leer mientras otra escribe, lo que provocaría una condición de carrera de datos. Si las lecturas son mucho más frecuentes que las escrituras, usa sync.RWMutex en su lugar y llama a RLock() para las lecturas.

Un principio de diseño clave: mantén el mutex privado. Al usar un nombre de campo en minúsculas (mu), impides que el código externo acceda a él directamente. Toda la sincronización se realiza mediante tus métodos, lo que te proporciona un control total sobre la seguridad de los hilos.

Para las estructuras con varios campos, protege todos los campos relacionados con el mismo mutex para garantizar un estado coherente:

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

Tanto balance como history se actualizan atómicamente: ninguna goroutine puede observar un estado incoherente en el que uno haya cambiado, pero el otro no.

challenge icon

Desafío

Fácil

Construyamos un sistema de cuenta bancaria seguro para subprocesos que demuestre la encapsulación adecuada de la sincronización dentro de los métodos de una estructura. Tu cuenta gestionará de forma segura depósitos, retiros y comprobaciones de saldo simultáneos sin exponer ningún detalle de bloqueo a quienes la utilicen.

Organizarás tu código en dos archivos:

  • account.go: Define tu cuenta bancaria segura para subprocesos.

    Crea una estructura BankAccount con un sync.Mutex incrustado, un campo balance (int) y un slice transactions que registre todas las operaciones exitosas como cadenas.

    Implementa estos métodos:

    • NewBankAccount(initial int) *BankAccount - Crea una cuenta nueva con el saldo inicial indicado y un slice de transacciones vacío
    • Deposit(amount int) - Suma el importe al saldo y registra la transacción como +[amount]
    • Withdraw(amount int) bool - Si hay fondos suficientes, resta el importe, registra -[amount] y devuelve true. De lo contrario, devuelve false sin modificar nada
    • Balance() int - Devuelve el saldo actual
    • History() []string - Devuelve una copia del slice de transacciones

    Todo método que acceda a los campos de la estructura debe bloquear el mutex para garantizar la seguridad de los subprocesos. Usa defer para desbloquearlo. Mantén el mutex y todos los campos sin exportar (en minúsculas) para que el código externo tenga que usar tus métodos.

  • main.go: Procesa las operaciones bancarias y muestra tu cuenta segura para subprocesos.

    Lee el saldo inicial y después el número de operaciones. Para cada operación, lee el tipo (deposit, withdraw o balance) y, para depositar o retirar, lee el importe.

    Imprime los resultados de cada operación:

    • deposit: Imprime Deposited [amount], Balance: [new balance]
    • withdraw: Imprime Withdrew [amount], Balance: [new balance] si tiene éxito, o Withdrawal failed: insufficient funds si no
    • balance: Imprime Current balance: [balance]

    Después de todas las operaciones, imprime el historial de transacciones, con cada entrada en una línea nueva y con el prefijo History: únicamente en la primera entrada.

Se proporcionarán las siguientes entradas:

  • Línea 1: Saldo inicial (entero)
  • Línea 2: Número de operaciones (entero)
  • Líneas siguientes: Para cada operación, el tipo (deposit, withdraw o balance) y, para depositar o retirar, el importe en la línea siguiente

Por ejemplo, dada la siguiente entrada:

100
5
deposit
50
balance
withdraw
30
withdraw
200
balance

Tu salida debería ser:

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

El principio clave aquí es que toda la sincronización está oculta dentro de tus métodos de BankAccount. Quienes utilicen la cuenta simplemente usan Deposit(), Withdraw() y Balance() sin tener que pensar jamás en los bloqueos. Tu estructura gestiona internamente la seguridad de los subprocesos.

Pruébalo tú mismo

package main

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

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

	// Leer el saldo inicial
	initialStr, _ := reader.ReadString('\n')
	initial, _ := strconv.Atoi(strings.TrimSpace(initialStr))

	// Leer el número de operaciones
	numOpsStr, _ := reader.ReadString('\n')
	numOps, _ := strconv.Atoi(strings.TrimSpace(numOpsStr))

	// Crear la cuenta bancaria
	account := NewBankAccount(initial)

	// Procesar cada operación
	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: Llama a Deposit e imprime el resultado
			// Formato: "Deposited [amount], Balance: [new balance]"

		case "withdraw":
			amountStr, _ := reader.ReadString('\n')
			amount, _ := strconv.Atoi(strings.TrimSpace(amountStr))
			// TODO: Llama a Withdraw e imprime el resultado apropiado
			// Si es exitoso: "Withdrew [amount], Balance: [new balance]"
			// If failed: "Withdrawal failed: insufficient funds"
			_ = amount // Elimina esta línea cuando lo implementes

		case "balance":
			// TODO: Llama a Balance e imprime el resultado
			// Format: "Current balance: [balance]"
		}
	}

	// TODO: Imprime el historial de transacciones
	// La primera entrada debe ir prefijada con "History: "
	// Las entradas posteriores deben estar en líneas nuevas sin prefijo
}
quiz iconPonte a prueba

Esta lección incluye un breve cuestionario. Empieza la lección para responderlo y registrar tu progreso.

Todas las lecciones de Programación Orientada a Objetos

Practica por tu cuenta: Compilador de Go online