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 structs que sean seguros de usar desde múltiples goroutines simultáneamente. Un struct seguro para hilos (thread-safe) encapsula la sincronización dentro de sus métodos, de modo que quienes los llaman no tengan que preocuparse por el bloqueo (locking).

El patrón es sencillo: incrusta 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
}

Ten en cuenta que incluso el método de solo lectura Value() bloquea el mutex. Sin esto, una goroutine podría leer mientras otra escribe, provocando una condición de carrera (data race). Si las lecturas son mucho más comunes que las escrituras, utiliza sync.RWMutex en su lugar y llama a RLock() para las lecturas.

Un principio de diseño clave: mantener el mutex privado. Al usar un nombre de campo en minúsculas (mu), evitas que el código externo acceda a él directamente. Toda la sincronización ocurre a través de tus métodos, lo que te otorga un control total sobre la seguridad de hilos (thread safety).

Para estructuras con múltiples campos, proteja todos los campos relacionados con el mismo mutex para garantizar un estado consistente:

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 de forma atómica—ninguna goroutine puede observar un estado inconsistente donde uno haya cambiado pero el otro no.

challenge icon

Desafío

Fácil

Vamos a construir un sistema de cuenta bancaria seguro para hilos (thread-safe) que demuestre una encapsulación adecuada de la sincronización dentro de los métodos de una estructura. Tu cuenta manejará depósitos, retiros y consultas de saldo concurrentes de forma segura 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 hilos.

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

    Implementa estos métodos:

    • NewBankAccount(initial int) *BankAccount - Crea una nueva cuenta con el saldo inicial dado y un slice de transacciones vacío.
    • Deposit(amount int) - Suma el monto al saldo y registra la transacción como +[amount].
    • Withdraw(amount int) bool - Si existen fondos suficientes, resta el monto, 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.

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

  • main.go: Procesa las operaciones bancarias y demuestra tu cuenta segura para hilos.

    Lee el saldo inicial y luego el número de operaciones. Para cada operación, lee el tipo (deposit, withdraw o balance) y, para deposit/withdraw, lee el monto.

    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, precedida por History: solo para 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 deposit/withdraw, el monto en la línea siguiente.

Por ejemplo, dado:

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 llaman simplemente usan Deposit(), Withdraw() y Balance() sin pensar nunca en bloqueos; tu estructura maneja la seguridad de hilos internamente.

Hoja de referencia

Una struct segura para hilos (thread-safe) encapsula la sincronización dentro de sus métodos incrustando un mutex y bloqueándolo en cada método que accede 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
}

Principios clave:

  • Bloquear el mutex en todos los métodos que acceden al estado compartido, incluyendo los métodos de solo lectura, para prevenir condiciones de carrera (data races)
  • Usar defer para asegurar que el mutex se desbloquee incluso si la función retorna antes de tiempo
  • Mantener el mutex privado (nombre de campo en minúscula) para que el código externo no pueda acceder a él directamente
  • Para cargas de trabajo con muchas lecturas, usar sync.RWMutex y llamar a RLock() para las lecturas

Para structs con múltiples campos, proteja todos los campos relacionados con el mismo mutex para asegurar un estado consistente:

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

Esto asegura que tanto balance como history se actualicen de forma atómica; ninguna goroutine puede observar un estado inconsistente.

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: Llamar a Deposit e imprimir el resultado
			// Formato: "Deposited [amount], Balance: [new balance]"

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

		case "balance":
			// TODO: Llamar a Balance e imprimir el resultado
			// Formato: "Current balance: [balance]"
		}
	}

	// TODO: Imprimir el historial de transacciones
	// La primera entrada debe tener el prefijo "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