Concorrência e Multithreading em C++: Nível Avançado
\n\nAplicações modernas exigem desempenho máximo, e o multithreading em C++ é uma das ferramentas-chave para alcançá-lo. No entanto, mecanismos básicos (std::thread, std::mutex) frequentemente levam a bloqueios, condições de corrida e redução de eficiência. Neste artigo, exploraremos técnicas avançadas de trabalho com concorrência em C++: desde operações atômicas até estruturas de dados lock-free e tarefas assíncronas.
\n\n1. Operações Atômicas e Memory Ordering
\n\nMutexes clássicos são um mecanismo pesado. Para contadores ou flags simples, é melhor usar std::atomic. Ele garante atomicidade de leitura/escrita sem bloqueios. No entanto, em nível avançado, é importante entender o memory ordering — o modelo de memória do C++ que define a visibilidade de alterações entre threads.
Principais ordens de memória:
\n- \n
memory_order_relaxed— garantias mínimas, apenas atomicidade. \n memory_order_consume— raramente usado, depende dos dados. \n memory_order_acquire— todas as leituras/escritas subsequentes não podem ser reordenadas antes desta operação. \n memory_order_release— todas as escritas anteriores tornam-se visíveis para outros threads após esta operação. \n memory_order_acq_rel— combinação de acquire e release. \n memory_order_seq_cst— o mais rigoroso, consistência sequencial global (padrão). \n
Exemplo 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; // escrita dos dados\n ready.store(true, std::memory_order_release); // publica a flag\n}\n\nvoid consumer() {\n while (!ready.load(std::memory_order_acquire)) // aguarda a flag\n ;\n assert(data == 42); // garante 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\nAqui, release e acquire garantem que a escrita de data ocorra antes da definição da flag, e a leitura, depois. Isso é mais rápido que um mutex.
2. Estruturas de Dados Lock-Free
\n\nAlgoritmos lock-free permitem evitar bloqueios usando operações atômicas e CAS (Compare-And-Swap). Um exemplo clássico é uma pilha lock-free baseada em lista encadeada simples.
\n\nImplementação de uma pilha 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 // loop até o CAS ter sucesso\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 // aguarda\n }\n if (old_head == nullptr) return false;\n result = old_head->data;\n // Atenção: em código real, é necessário um coletor de lixo ou hazard pointers\n delete old_head;\n return true;\n }\n};\n\nObservações importantes:
\n- \n