1. Objetivo del proyecto
El objetivo es construir un sistema de transferencias bancarias que:
- Tenga muchas cuentas (por ejemplo, 100).
- Procese muchas transferencias concurrentes (por ejemplo, 1000).
- No pierda dinero (el saldo total se conserva).
- No se bloquee (sin deadlocks).
- Verifique el saldo total al final.
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:
- Exclusión mutua: cada cuenta tiene su propio mutex.
- Orden de bloqueo: los locks se adquieren en orden de índice de cuenta.
- Verificación: el saldo total se verifica al final.
3. La estructura de la 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:
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:
- Hilo 1: lock(A) → lock(B). orden A-B
- Hilo 2: lock(B) → lock(A). orden B-A
- 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.
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
#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-safe | Sí | Cada cuenta tiene su propio mutex. |
| Sin deadlock | Sí | Orden de bloqueo por índice de cuenta. |
| Sin pérdida de dinero | Sí | La transferencia es atómica (ambos locks). |
| Verificación | Sí | El saldo total se verifica al final. |
| Escalable | Sí | 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
| Paso | Origen | Destino | Monto | Saldo origen | Saldo destino |
|---|
10. Resumen
- El proyecto implementa un sistema de transferencias bancarias concurrente.
- Cada cuenta tiene su propio mutex.
- El orden de bloqueo (por índice de cuenta) evita deadlocks.
- La transferencia es atómica: adquiere ambos locks, transfiere, libera.
- El saldo total se verifica al final para detectar errores.
- Las transferencias entre cuentas distintas se ejecutan en paralelo.
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
- Implementa el sistema de transferencias y verifica el saldo total.
- Ejecuta con ThreadSanitizer y verifica que no hay carreras.
- Intercambia el orden de los locks y observa el deadlock.
- Añade un contador de transferencias exitosas y fallidas.
- Compara el rendimiento con y sin orden de bloqueo.
- Investiga cómo se implementan las transferencias en bases de datos (transacciones).