Uso de atomic en C++ para aplicaciones multihilo

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_weak y compare_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.

Etiquetas: C++ atomic atomic_flag Multithreading lock-free

Publicado el 7-23 14:14