Introdução à concorrência e multithreading em C++
Os processadores modernos possuem múltiplos núcleos e, para usar toda a sua potência, os programas devem ser capazes de executar várias tarefas simultaneamente. Concorrência (concurrency) é a capacidade de um programa executar várias tarefas em períodos de tempo sobrepostos, e multithreading é uma das formas de implementá-la, onde o programa é dividido em várias threads trabalhando em paralelo.
Em C++, o suporte a multithreading foi introduzido no padrão C++11 e desde então tem sido ativamente desenvolvido. Neste artigo, abordaremos todos os aspectos principais: desde a criação de threads até padrões complexos de sincronização.
Fundamentos: criação e gerenciamento de threads
std::thread — o bloco de construção básico
A classe std::thread permite executar uma função ou objeto chamável em uma thread separada. Vejamos um exemplo simples:
#include <iostream>#include <thread>
void hello() { std::cout << "Olá da thread " << std::this_thread::get_id() << std::endl;}
int main() { std::thread t(hello); // Inicia a thread t.join(); // Aguarda a conclusão da thread return 0;}O método join() faz a thread principal aguardar a conclusão da thread filha. A alternativa é detach(), que permite que a thread funcione de forma independente, mas requer cuidado especial (a thread pode sobreviver aos objetos que a criaram).
Passagem de argumentos e funções lambda
É possível passar argumentos para as threads e usar lambdas:
#include <thread>#include <string>
void process(int id, const std::string& data) { // Processamento de dados}
int main() { std::thread t1(process, 1, "dados"); std::thread t2([](int x) { // Expressão lambda }, 42); t1.join(); t2.join();}Sincronização de threads: protegendo dados compartilhados
Quando várias threads acessam os mesmos dados, ocorre uma condição de corrida (race condition). Para evitá-la, são usados mecanismos de sincronização.
Mutexes (std::mutex)
Um mutex é um primitivo de sincronização que garante que apenas uma thread pode possuí-lo a qualquer momento:
#include <iostream>#include <thread>#include <mutex>
std::mutex mtx;int counter = 0;
void increment() { for (int i = 0; i < 100000; ++i) { std::lock_guard<std::mutex> lock(mtx); ++counter; // Acesso protegido }}
int main() { std::thread t1(increment); std::thread t2(increment); t1.join(); t2.join(); std::cout << "Contador: " << counter << std::endl; return 0;}std::lock_guard é um wrapper RAII que captura automaticamente o mutex ao ser criado e o libera ao ser destruído. Isso protege contra o esquecimento acidental de liberar o mutex.
Variáveis de condição (std::condition_variable)
Variáveis de condição permitem que as threads aguardem a ocorrência de um determinado evento:
#include <queue>#include <condition_variable>
std::queue<int> data_queue;std::mutex queue_mutex;std::condition_variable cv;
void producer() { for (int i = 0; i < 10; ++i) { std::this_thread::sleep_for(std::chrono::milliseconds(100)); std::lock_guard<std::mutex> lock(queue_mutex); data_queue.push(i); cv.notify_one(); // Notifica o consumidor }}
void consumer() { while (true) { std::unique_lock<std::mutex> lock(queue_mutex); cv.wait(lock, []{ return !data_queue.empty(); }); int value = data_queue.front(); data_queue.pop(); lock.unlock(); // Processa