Concurrencia y Multihilo en C++: Nivel Avanzado
\n\nLas aplicaciones modernas requieren el máximo rendimiento, y el multihilo en C++ es una de las herramientas clave para lograrlo. Sin embargo, los mecanismos básicos (std::thread, std::mutex) a menudo generan bloqueos, condiciones de carrera y reducción de la eficiencia. En este artículo, exploraremos técnicas avanzadas para trabajar con concurrencia en C++: desde operaciones atómicas hasta estructuras de datos lock-free y tareas asíncronas.
\n\n1. Operaciones Atómicas y Ordenamiento de Memoria
\n\nLos mutex clásicos son un mecanismo pesado. Para contadores simples o banderas, es mejor usar std::atomic. Garantiza la atomicidad de lectura/escritura sin bloqueos. Sin embargo, a nivel avanzado es importante entender el memory ordering (ordenamiento de memoria): el modelo de memoria de C++ que define la visibilidad de los cambios entre hilos.
Órdenes de memoria principales:
\n- \n
memory_order_relaxed— garantías mínimas, solo atomicidad. \n memory_order_consume— se usa raramente, depende de los datos. \n memory_order_acquire— todas las lecturas/escrituras posteriores no pueden reordenarse antes de esta operación. \n memory_order_release— todas las escrituras anteriores se vuelven visibles para otros hilos después de esta operación. \n memory_order_acq_rel— combinación de acquire y release. \n memory_order_seq_cst— el más estricto, consistencia secuencial global (por defecto). \n
Ejemplo de uso:
\n#include <atomic>\n#include <thread>\n#include <cassert>\n\nstd::atomic<bool> ready{false};\nint data = 0;\n\nvoid producer() {\n data = 42; // escritura de datos\n ready.store(true, std::memory_order_release); // publicamos la bandera\n}\n\nvoid consumer() {\n while (!ready.load(std::memory_order_acquire)) // esperamos la bandera\n ;\n assert(data == 42); // garantizado que veremos data == 42\n}\n\nint main() {\n std::thread t1(producer);\n std::thread t2(consumer);\n t1.join();\n t2.join();\n return 0;\n}\n\nAquí, release y acquire garantizan que la escritura de data ocurra antes de establecer la bandera, y la lectura después. Esto es más rápido que un mutex.
2. Estructuras de Datos Lock-Free
\n\nLos algoritmos lock-free evitan bloqueos utilizando operaciones atómicas y CAS (Compare-And-Swap). Un ejemplo clásico es una pila lock-free basada en una lista enlazada simple.
\n\nImplementación de una pila lock-free:
\n#include <atomic>\n#include <memory>\n\ntemplate<typename T>\nclass LockFreeStack {\nprivate:\n struct Node {\n T data;\n Node* next;\n Node(const T& val) : data(val), next(nullptr) {}\n };\n std::atomic<Node*> head{nullptr};\n\npublic:\n void push(const T& val) {\n Node* new_node = new Node(val);\n new_node->next = head.load(std::memory_order_relaxed);\n while (!head.compare_exchange_weak(\n new_node->next, new_node,\n std::memory_order_release,\n std::memory_order_relaxed)) {\n // bucle hasta que CAS tenga éxito\n }\n }\n\n bool pop(T& result) {\n Node* old_head = head.load(std::memory_order_relaxed);\n while (old_head != nullptr &&\n !head.compare_exchange_weak(\n old_head, old_head->next,\n std::memory_order_acquire,\n std::memory_order_relaxed)) {\n // esperamos\n }\n if (old_head == nullptr) return false;\n result = old_head->data;\n // Atención: en código real se necesita un recolector de basura o hazard pointers\n delete old_head;\n return true;\n }\n};\n\nNotas importantes:
\n- \n