Mutexes, Locks y Variables de Condicion
Protege datos compartidos con mutexes, lock guards y unique locks. Usa variables de condicion para senalizacion entre hilos y evita trampas comunes de deadlock.
El Problema: Carreras de Datos
Una carrera de datos ocurre cuando dos o mas hilos acceden al mismo dato concurrentemente y al menos un acceso es una escritura, sin sincronizacion. Las carreras de datos son comportamiento indefinido — no solo datos incorrectos, sino UB genuino que puede causar fallos, datos corruptos o apariencia de funcionamiento correcto por pura suerte.
int counter = 0;
void increment() { counter++; } // carrera de datos si se llama desde multiples hiloscounter++ parece atomico pero en realidad son tres operaciones: cargar, incrementar, almacenar. Dos hilos pueden cargar el mismo valor, incrementar a la misma respuesta y almacenar, perdiendo un incremento.
La solucion: sincronizacion — mutexes, locks, variables de condicion y atomicos aseguran que las secciones criticas se ejecuten de forma mutuamente exclusiva.
std::mutex y Envoltorios de Lock
Un std::mutex proporciona exclusion mutua — solo un hilo puede bloquearlo a la vez. std::lock_guard y std::unique_lock son envoltorios RAII que aseguran que el mutex siempre se desbloquee, incluso si se lanzan excepciones.
#include <iostream>
#include <thread>
#include <mutex>
#include <vector>
std::mutex mtx;
int shared_counter = 0;
void increment(int times) {
for (int i = 0; i < times; ++i) {
std::lock_guard<std::mutex> lock(mtx); // RAII lock
++shared_counter; // safe: only one thread at a time
} // lock released here automatically
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; ++i) {
threads.emplace_back(increment, 10000);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Counter: " << shared_counter << "\n";
// Always prints 100000 — no data race
return 0;
}std::scoped_lock — Multi-Lock a Prueba de Deadlocks (C++17)
std::scoped_lock puede bloquear multiples mutexes simultaneamente usando un algoritmo que evita deadlocks. Siempre usa scoped_lock cuando necesites bloquear mas de un mutex.
#include <iostream>
#include <thread>
#include <mutex>
#include <chrono>
std::mutex mtx;
void try_lock_example() {
// Deferred locking — don't lock yet
std::unique_lock<std::mutex> lock(mtx, std::defer_lock);
// Try to lock with a timeout
if (lock.try_lock_for(std::chrono::milliseconds(100))) {
std::cout << "Acquired lock\n";
// ... do work ...
} else {
std::cout << "Could not acquire lock within 100ms\n";
}
// Automatically unlocks when lock goes out of scope
}
void manual_control() {
std::unique_lock<std::mutex> lock(mtx);
// ... critical section ...
lock.unlock(); // release early if needed
// ... non-critical work ...
lock.lock(); // re-acquire
// ... another critical section ...
}Variables de Condicion: Esperar Eventos
Una std::condition_variable permite que un hilo espere hasta que otro hilo senalice que alguna condicion es verdadera. Siempre usa una variable de condicion con un mutex y un predicado (para proteger contra despertares espureos).
#include <mutex>
#include <thread>
#include <iostream>
struct BankAccount {
std::mutex mtx;
double balance = 1000.0;
};
void transfer(BankAccount& from, BankAccount& to, double amount) {
// Lock both mutexes without risk of deadlock
std::scoped_lock lock(from.mtx, to.mtx);
if (from.balance >= amount) {
from.balance -= amount;
to.balance += amount;
std::cout << "Transferred " << amount << "\n";
}
} // both mutexes released
int main() {
BankAccount alice, bob;
// These two transfers lock accounts in different order,
// but scoped_lock prevents deadlock.
std::thread t1(transfer, std::ref(alice), std::ref(bob), 100.0);
std::thread t2(transfer, std::ref(bob), std::ref(alice), 50.0);
t1.join();
t2.join();
std::cout << "Alice: " << alice.balance
<< ", Bob: " << bob.balance << "\n";
return 0;
}Tipos de Mutex y Patrones de Lock
La STL proporciona varios tipos de mutex:
- std::mutex — Lock basico, no recursivo. Bloquear dos veces desde el mismo hilo es UB.
- std::recursive_mutex — El mismo hilo puede bloquearlo multiples veces; debe desbloquearlo el mismo numero de veces.
- std::timed_mutex / std::recursive_timed_mutex — Soportan try_lock_for(duration) y try_lock_until(time_point) para bloqueo temporizado.
- std::shared_mutex (C++17) — Lock de lectores-escritor. Multiples hilos pueden mantener un lock compartido (lectura) simultaneamente, pero solo un hilo puede mantener un lock exclusivo (escritura).
Envoltorios de lock: Usa std::lock_guard para el caso simple (bloquear en construccion, desbloquear en destruccion). Usa std::unique_lock cuando necesites desbloqueo manual, bloqueo diferido, transferencia o uso con variables de condicion. Usa std::shared_lock para lectores con std::shared_mutex.
Un deadlock ocurre cuando dos o mas hilos se bloquean mutuamente, cada uno esperando que el otro libere un lock.
// Hilo 1: bloquea A, luego intenta bloquear B
// Hilo 2: bloquea B, luego intenta bloquear A
// -> DEADLOCK — ambos hilos esperan para siempreReglas para prevenir deadlocks:
1. Bloquea mutexes en un orden consistente a traves de todos los hilos
2. Usa std::scoped_lock(m1, m2) — adquiere ambos locks sin riesgo de deadlock usando un algoritmo especializado
3. Nunca llames a funciones externas mientras mantienes un lock — la funcion externa podria intentar adquirir otro lock
4. Mantén los locks el menor tiempo posible — bloquea solo la seccion critica, no calculos completos
5. Nunca esperes en una variable de condicion mientras mantienes multiples locks — esto frecuentemente causa deadlocks
Prefiere std::scoped_lock para secciones criticas simples y std::unique_lock cuando necesites variables de condicion o transferencia de lock. Usa std::shared_mutex cuando las lecturas superan ampliamente a las escrituras. Mantén los datos protegidos y su mutex juntos en un struct — esto hace obvia la relacion. Siempre pasa un predicado a condition_variable::wait() para manejar despertares espureos. Considera alternativas sin lock (std::atomic) para datos compartidos simples como contadores.
- Una carrera de datos (lectura/escritura concurrente sin sincronizacion) es comportamiento indefinido — no solo datos incorrectos
std::lock_guardystd::scoped_lockson envoltorios RAII que aseguran que los mutexes siempre se desbloqueenstd::scoped_lockbloquea multiples mutexes sin riesgo de deadlock — siempre usalo para multi-lock- Las variables de condicion necesitan un mutex + un predicado — el predicado protege contra despertares espureos
std::shared_mutexhabilita locks de lectores-escritor — multiples lectores concurrentes, escritores exclusivos- Previene deadlocks con orden consistente de locks,
std::scoped_locky secciones criticas minimas
Quiz — Pon a prueba tus conocimientos
(20 XP)1. Que sucede si dos hilos incrementan un `int` compartido sin sincronizacion?
2. Como previene `std::scoped_lock` deadlocks al bloquear multiples mutexes?
3. Por que las variables de condicion requieren un predicado en la llamada a `wait()`?