← Concurrencia y Sincronización Bloque 10 · Integración y cierre · Tema 46 de 47

Tema 46

Proyecto: transferencias bancarias

En este proyecto construirás un sistema de transferencias bancarias concurrente: muchas cuentas, muchos hilos, sin pérdidas de dinero ni interbloqueos, y con verificación del saldo total. Es el proyecto que integra mutex, orden de bloqueo, detección de carreras y verificación.

1. Objetivo del proyecto

El objetivo es construir un sistema de transferencias bancarias que:

2. El problema

El problema de las transferencias bancarias es un clásico de la concurrencia: si dos hilos transfieren dinero entre las mismas cuentas simultáneamente, sin sincronización, el saldo puede corromperse. Además, si los hilos adquieren los locks de las cuentas en orden diferente, puede haber deadlock.

La solución requiere:

3. La estructura de la cuenta

cuenta
typedef struct {
    int id;
    int saldo;
    pthread_mutex_t mutex;
} cuenta_t;

Cada cuenta tiene un mutex propio. Esto permite que las transferencias entre cuentas distintas se ejecuten en paralelo (si no hay contención por la misma cuenta).

4. La transferencia

Una transferencia adquiere los locks de ambas cuentas, verifica que haya suficiente saldo, realiza la transferencia y libera los locks:

transferir
void transferir(cuenta_t *origen, cuenta_t *destino, int monto) {
    /* Adquirir locks en orden de índice. */
    if (origen->id < destino->id) {
        pthread_mutex_lock(&origen->mutex);
        pthread_mutex_lock(&destino->mutex);
    } else {
        pthread_mutex_lock(&destino->mutex);
        pthread_mutex_lock(&origen->mutex);
    }

    if (origen->saldo >= monto) {
        origen->saldo -= monto;
        destino->saldo += monto;
    }

    pthread_mutex_unlock(&origen->mutex);
    pthread_mutex_unlock(&destino->mutex);
}

El orden de adquisición (por índice de cuenta) evita el deadlock: no puede haber un ciclo si todos adquieren en el mismo orden.

5. Orden de bloqueo

El orden de bloqueo es esencial para evitar deadlocks. Si el hilo 1 transfiere de A a B y el hilo 2 transfiere de B a A, y ambos adquieren los locks en orden diferente, se produce un deadlock:

  1. Hilo 1: lock(A) → lock(B). orden A-B
  2. Hilo 2: lock(B) → lock(A). orden B-A
  3. Deadlock: Hilo 1 tiene A, espera B. Hilo 2 tiene B, espera A. ciclo

Con orden de bloqueo (siempre el de menor índice primero), ambos hilos adquieren A primero y luego B. No hay ciclo.

6. Verificación del saldo total

La verificación del saldo total es la prueba de que el sistema es correcto: la suma de los saldos de todas las cuentas debe ser igual al saldo inicial, independientemente de las transferencias realizadas.

verificación
int saldo_total(cuenta_t *cuentas, int n) {
    int total = 0;
    for (int i = 0; i < n; i++) {
        pthread_mutex_lock(&cuentas[i].mutex);
        total += cuentas[i].saldo;
        pthread_mutex_unlock(&cuentas[i].mutex);
    }
    return total;
}

Si el saldo total no coincide con el inicial, hay una condición de carrera o un bug en la transferencia.

7. Código completo

transferencias.c
#define _POSIX_C_SOURCE 200809L
#include <pthread.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

#define NUM_CUENTAS 10
#define SALDO_INICIAL 1000
#define NUM_TRANSFERENCIAS 1000
#define NUM_HILOS 8

typedef struct {
    int id;
    int saldo;
    pthread_mutex_t mutex;
} cuenta_t;

cuenta_t cuentas[NUM_CUENTAS];

void cuenta_init(cuenta_t *c, int id, int saldo) {
    c->id = id;
    c->saldo = saldo;
    pthread_mutex_init(&c->mutex, NULL);
}

void transferir(int origen, int destino, int monto) {
    if (origen == destino) return;
    /* Orden de bloqueo: menor índice primero. */
    int primero = origen < destino ? origen : destino;
    int segundo = origen < destino ? destino : origen;

    pthread_mutex_lock(&cuentas[primero].mutex);
    pthread_mutex_lock(&cuentas[segundo].mutex);

    if (cuentas[origen].saldo >= monto) {
        cuentas[origen].saldo -= monto;
        cuentas[destino].saldo += monto;
    }

    pthread_mutex_unlock(&cuentas[segundo].mutex);
    pthread_mutex_unlock(&cuentas[primero].mutex);
}

void *hilo_transferencias(void *arg) {
    int id = *(int *)arg;
    unsigned int seed = id;
    for (int i = 0; i < NUM_TRANSFERENCIAS / NUM_HILOS; i++) {
        int origen = rand_r(&seed) % NUM_CUENTAS;
        int destino = rand_r(&seed) % NUM_CUENTAS;
        int monto = 1 + rand_r(&seed) % 100;
        transferir(origen, destino, monto);
    }
    return NULL;
}

int saldo_total(void) {
    int total = 0;
    for (int i = 0; i < NUM_CUENTAS; i++) {
        pthread_mutex_lock(&cuentas[i].mutex);
        total += cuentas[i].saldo;
        pthread_mutex_unlock(&cuentas[i].mutex);
    }
    return total;
}

int main(void) {
    for (int i = 0; i < NUM_CUENTAS; i++) {
        cuenta_init(&cuentas[i], i, SALDO_INICIAL);
    }

    int saldo_inicial_total = NUM_CUENTAS * SALDO_INICIAL;
    printf("Saldo inicial total: %d\n", saldo_inicial_total);

    pthread_t hilos[NUM_HILOS];
    int ids[NUM_HILOS];
    for (int i = 0; i < NUM_HILOS; i++) {
        ids[i] = i;
        pthread_create(&hilos[i], NULL, hilo_transferencias, &ids[i]);
    }
    for (int i = 0; i < NUM_HILOS; i++) {
        pthread_join(hilos[i], NULL);
    }

    int saldo_final = saldo_total();
    printf("Saldo final total: %d\n", saldo_final);
    printf("Diferencia: %d\n", saldo_final - saldo_inicial_total);

    for (int i = 0; i < NUM_CUENTAS; i++) {
        pthread_mutex_destroy(&cuentas[i].mutex);
    }
    return 0;
}

8. Propiedades

Propiedad¿La cumple?Comentario
Thread-safeSíCada cuenta tiene su propio mutex.
Sin deadlockSíOrden de bloqueo por índice de cuenta.
Sin pérdida de dineroSíLa transferencia es atómica (ambos locks).
VerificaciónSíEl saldo total se verifica al final.
EscalableSíLas transferencias entre cuentas distintas se ejecutan en paralelo.

9. Simulador interactivo

Este simulador muestra cómo se realizan transferencias entre cuentas:

Simulador · transferencias bancarias

PasoOrigenDestinoMontoSaldo origenSaldo destino

10. Resumen

11. Preguntas de repaso

Intenta responder cada una antes de abrir la respuesta.

1. ¿Por qué cada cuenta tiene su propio mutex?

Para permitir que las transferencias entre cuentas distintas se ejecuten en paralelo.

2. ¿Por qué se usa orden de bloqueo?

Para evitar deadlocks cuando dos hilos transfieren entre las mismas cuentas en orden inverso.

3. ¿Cómo se verifica que no hay pérdidas?

Comparando el saldo total final con el inicial.

4. ¿Qué ocurre si no se usa orden de bloqueo?

Puede haber deadlock cuando dos hilos transfieren entre las mismas cuentas en orden inverso.

5. ¿Por qué se adquieren ambos locks antes de verificar el saldo?

Para que otro hilo no modifique el saldo entre la verificación y la transferencia.

6. ¿Qué ocurre si una transferencia falla (saldo insuficiente)?

No se realiza la transferencia; el saldo no cambia.

7. ¿Cómo se mide el rendimiento del sistema?

Con el número de transferencias por segundo.

8. ¿Qué herramienta detecta carreras en este programa?

ThreadSanitizer (con -fsanitize=thread).

9. ¿Por qué se usa rand_r en lugar de rand?

Porque rand no es thread-safe (usa estado compartido).

10. ¿Qué bloque del curso cierra este proyecto?

El bloque 10 (integración y cierre).

Para practicar

  1. Implementa el sistema de transferencias y verifica el saldo total.
  2. Ejecuta con ThreadSanitizer y verifica que no hay carreras.
  3. Intercambia el orden de los locks y observa el deadlock.
  4. Añade un contador de transferencias exitosas y fallidas.
  5. Compara el rendimiento con y sin orden de bloqueo.
  6. Investiga cómo se implementan las transferencias en bases de datos (transacciones).