En escenarios que requieren alto rendimiento y eficiencia, a menudo es necesario implementar algoritmos y estructuras de datos sin bloqueo (lock-free). C++ proporciona tipos atómicos para gestionar operaciones concurrentes de manera segura.
atomic_flag
El tipo atomic_flag es una bandera booleana atómica que solo soporta las operaciones test-and-set y clear.
Constructor
Solo tiene un constructor por defecto; no se puede copiar de otro objeto. Un objeto atomic_flag debe inicializarse con ATOMIC_FLAG_INIT para dejarlo en estado clear; de lo contrario, su estado es indeterminado.
atomic_flag() noexcept = default;
atomic_flag(const atomic_flag&) = delete;
Ejemplo: Se crean 10 hilos que realizan un conteo; el primero en terminar imprime su identificador.
#include <iostream>
#include <thread>
#include <atomic>
#include <vector>
std::atomic<bool> start_signal(false);
std::atomic_flag first_winner = ATOMIC_FLAG_INIT;
void count_to_million(int thread_id) {
while (!start_signal) {
std::this_thread::yield();
}
for (volatile int i = 0; i < 1000000; ++i) {
// Simulación de trabajo
}
if (!first_winner.test_and_set()) {
std::cout << "Hilo " << thread_id << " completó primero.\n";
}
}
int main() {
std::vector<std::thread> threads;
std::cout << "Iniciando 10 hilos que cuentan hasta 1 millón...\n";
for (int i = 1; i <= 10; ++i) {
threads.emplace_back(count_to_million, i);
}
start_signal = true;
for (auto& t : threads) {
t.join();
}
return 0;
}
std::atomic_flag::test_and_set
Verifica la bandera: si estaba establecida, retorna true; si no, retorna false y la establece. Es una operación atómica con un orden de memoria configurable.
bool test_and_set(memory_order sync = memory_order_seq_cst) volatile noexcept;
bool test_and_set(memory_order sync = memory_order_seq_cst) noexcept;
Los órdenes de memoria disponibles son: memory_order_relaxed, memory_order_consume, memory_order_acquire, memory_order_release, memory_order_acq_rel y memory_order_seq_cst.
Ejemplo: Uso como bloqueo simple para sincronizar la escritura en un flujo.
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
#include <sstream>
std::atomic_flag stream_lock = ATOMIC_FLAG_INIT;
std::ostringstream output_stream;
void write_to_stream(int id) {
while (stream_lock.test_and_set()) {
// Espera activa
}
output_stream << "Hilo " << id << " ha escrito.\n";
stream_lock.clear();
}
int main() {
std::vector<std::thread> workers;
for (int i = 1; i <= 5; ++i) {
workers.emplace_back(write_to_stream, i);
}
for (auto& w : workers) {
w.join();
}
std::cout << output_stream.str();
return 0;
}
std::atomic_flag::clear
Reinicia la bandera a false. Es una operación atómica con orden de memoria configurable.
void clear(memory_order sync = memory_order_seq_cst) volatile noexcept;
void clear(memory_order sync = memory_order_seq_cst) noexcept;
atomic_flag puede usarse como un spinlock básico.
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>
std::atomic_flag spinlock = ATOMIC_FLAG_INIT;
void print_message(int num) {
for (int iter = 0; iter < 3; ++iter) {
while (spinlock.test_and_set(std::memory_order_acquire)) {
// Giro hasta adquirir el bloqueo
}
std::cout << "Salida del hilo " << num << "\n";
spinlock.clear(std::memory_order_release);
}
}
int main() {
std::vector<std::thread> threads;
for (int n = 0; n < 4; ++n) {
threads.emplace_back(print_message, n);
}
for (auto& t : threads) {
t.join();
}
return 0;
}
atomic
El tipo atomic_flag es limitado; std::atomic ofrece funcionalidad completa para tipos genéricos. Los objetos atómicos evitan condiciones de carrera entre hilos.
Constructor
std::atomic es una plantilla de clase con especializaciones para tipos integrales y punteros.
template<class T>
struct atomic;
template<>
struct atomic<integral> {};
template<class T>
struct atomic<T*> {};
El constructor por defecto deja el objeto sin inicializar; se requiere atomic_init. El constructor constexpr inicializa con un valor. La copia está deshabilitada.
#include <iostream>
#include <atomic>
#include <thread>
#include <vector>
std::atomic<bool> go_signal(false);
std::atomic_flag winner = ATOMIC_FLAG_INIT;
void execute_task(int id) {
while (!go_signal) {
std::this_thread::yield();
}
for (volatile int i = 0; i < 500000; ++i) {
// Procesamiento
}
if (!winner.test_and_set()) {
std::cout << "Tarea " << id << " finalizada primero.\n";
}
}
int main() {
std::vector<std::thread> tasks;
std::cout << "Lanzando 10 tareas concurrentes...\n";
for (int i = 1; i <= 10; ++i) {
tasks.emplace_back(execute_task, i);
}
go_signal = true;
for (auto& t : tasks) {
t.join();
}
return 0;
}
Funciones miembro
El operador de asignación operator= permite asignar valores del tipo subyacente de forma atómica, con orden de memoria por defecto memory_order_seq_cst.
#include <iostream>
#include <atomic>
#include <thread>
std::atomic<int> counter(0);
void increment(int val) {
counter = val;
}
void wait_and_print() {
while (counter == 0) {
std::this_thread::yield();
}
std::cout << "Valor: " << counter.load() << "\n";
}
int main() {
std::thread t1(wait_and_print);
std::thread t2(increment, 42);
t1.join();
t2.join();
return 0;
}
Otras funciones importantes:
is_lock_free(): determina si el objeto es lock-free.store(): almacena un valor con orden de memoria configurable (relaxed, release, seq_cst).load(): carga el valor con orden de memoria configurable (relaxed, consume, acquire, seq_cst).exchange(): intercambia valores atómicamente.compare_exchange_weakycompare_exchange_strong: operaciones de intercambio condicional.
Para tipos integrales y punteros, std::atomic ofrece funciones especializadas como fetch_add, fetch_sub, fetch_and, fetch_or, fetch_xor, operator++ y operator--.
API estilo C
C++11 también proporciona una API de funciones atómicas estilo C junto con los tipos std::atomic y std::atomic_flag.