C++: Sincronização de Multithreading
Última atualização: 2026-08-26
Na aula 43, aprendemos o básico de multithreading.
Agora, vamos nos aprofundar na sincronização de multithreading — fazendo múltiplas threads coordenarem seu trabalho e evitando corridas e deadlocks.
O desafio da programação multithreaded não é criar threads — é a sincronização.
1. Deadlock
(1) 1.1 O Que É um Deadlock?
Um deadlock ocorre quando duas ou mais threads esperam uma pela outra para liberar recursos, então nenhuma pode continuar.
Quatro condições necessárias:
- Exclusão mútua: Recursos não podem ser compartilhados
- Posse e espera: Uma thread possuindo recursos espera por outros recursos
- Sem preempção: Recursos não podem ser liberados à força
- Espera circular: Existe uma cadeia circular de threads esperando
(2) 1.2 Exemplo de Deadlock
Exemplo: Deadlock (Dificuldade ⭐⭐⭐)
▶ Exemplo 2: Demo de programação multithreading (Dificuldade ⭐)
#include <iostream>
#include <thread>
#include <mutex>
std::mutex mtx1, mtx2;
void thread1() {
std::lock_guard<std::mutex> lock1(mtx1);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guard<std::mutex> lock2(mtx2); // Esperando por mtx2
std::cout << "Thread 1 done" << std::endl;
}
void thread2() {
std::lock_guardstd::mutex lock2(mtx2);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
std::lock_guardstd::mutex lock1(mtx1); // Esperando por mtx1
std::cout << "Thread 2 done" << std::endl;
}
int main() {
std::thread t1(thread1);
std::thread t2(thread2);
t1.join();
t2.join();
return 0;
}
Saída:
Thread 1 done
Thread 2 done
💡 Dica:
- Este programa pode nunca terminar de executar (deadlock)
(3) 1.3 Evitando Deadlocks
Método 1: Ordem de Bloqueio Fixa
void thread1() {
std::lock_guardstd::mutex lock1(mtx1);
std::lock_guardstd::mutex lock2(mtx2); // Sempre mtx1 primeiro, depois mtx2
std::cout << "Thread 1 done" << std::endl;
}
void thread2() {
std::lock_guardstd::mutex lock1(mtx1); // Mesma ordem que thread1
std::lock_guardstd::mutex lock2(mtx2);
std::cout << "Thread 2 done" << std::endl;
}
Método 2: Use std::lock para bloquear múltiplos mutexes simultaneamente
void safe_function() {
std::unique_lock<std::mutex> lock1(mtx1, std::defer_lock);
std::unique_lock<std::mutex> lock2(mtx2, std::defer_lock);
std::lock(lock1, lock2); // Bloqueia simultaneamente, evita deadlock
// ...
}
Método 3: Use std::scoped_lock (C++17, recomendado)
void safe_function() {
std::scoped_lock lock(mtx1, mtx2); // Evita deadlock automaticamente
// ...
}
2. Lock de Leitura-Escrita
(1) 2.1 Por Que Precisamos de Locks de Leitura-Escrita?
Problema: Mutexes são muito "conservadores" — mesmo quando múltiplas threads apenas leem dados, elas precisam esperar.
Solução: Lock de leitura-escrita (std::shared_mutex, C++17)
| Tipo de Lock | Propósito |
|---|---|
| Lock exclusivo | Operação de escrita, outras threads não podem ler ou escrever |
| Lock compartilhado | Operação de leitura, múltiplas threads podem ler simultaneamente |
(2) 2.2 Exemplo: Protegendo um cache com lock de leitura-escrita (Dificuldade ⭐⭐⭐)
#include <iostream>
#include <thread>
#include <shared_mutex>
#include <unordered_map>
std::unordered_map<int, int> cache;
std::shared_mutex smtx;
int get(int key) {
std::shared_lock<std::shared_mutex> lock(smtx); // Lock compartilhado (leitura)
auto it = cache.find(key);
if (it != cache.end()) {
return it->second;
}
return -1;
}
void set(int key, int value) {
std::unique_lock<std::shared_mutex> lock(smtx); // Lock exclusivo (escrita)
cache[key] = value;
}
int main() {
// Múltiplas threads podem ler simultaneamente; escritas são exclusivas
return 0;
}
Saída:
(Saída do programa)
3. Operações Atômicas
(1) 3.1 O Que É uma Operação Atômica?
Uma operação atômica é uma operação indivisível — ela executa completamente ou não executa.
Vantagem: Não precisa de locking, alto desempenho.
(2) 3.2 std::atomic
O C++11 fornece o template std::atomic.
Exemplo: Contador atômico (Dificuldade ⭐⭐)
#include <iostream>
#include <thread>
#include <vector>
#include <atomic>
std::atomic<int> counter(0); // Variável atômica
void increment() {
for (int i = 0; i < 1000; i++) {
counter++; // Operação atômica, sem lock necessário
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; i++) {
threads.emplace_back(increment);
}
for (auto& t : threads) {
t.join();
}
std::cout << "Counter: " << counter << std::endl; // Sempre 10000
return 0;
}
(3) 3.3 Ordem de Memória
std::atomic suporta ordem de memória para controlar o escopo da sincronização.
Ordens de memória comuns:
std::memory_order_relaxed: Mais fraca, apenas garante atomicidadestd::memory_order_acquire: Operação de leitura, impede reordenaçãostd::memory_order_release: Operação de escrita, impede reordenaçãostd::memory_order_seq_cst: Mais forte, padrão (consistência sequencial)
4. Pool de Threads
(1) 4.1 Por Que Precisamos de um Pool de Threads?
Problema: Criação/destruição frequente de threads tem alto overhead.
Solução: Pool de threads — pré-criar um conjunto de threads e reutilizá-las.
(2) 4.2 Implementação Simples de Pool de Threads
Exemplo: Pool de threads (Dificuldade ⭐⭐⭐⭐)
#include <iostream>
#include <thread>
#include <vector>
#include <queue>
#include <functional>
#include <mutex>
#include <condition_variable>
class ThreadPool {
private:
std::vector<std::thread> workers;
std::queue<std::function<void()>> tasks;
std::mutex mtx;
std::condition_variable cv;
bool stop;
public:
ThreadPool(size_t numThreads) : stop(false) {
for (size_t i = 0; i < numThreads; i++) {
workers.emplace_back([this] {
while (true) {
std::function<void()> task;
{
std::unique_lock<std::mutex> lock(mtx);
cv.wait(lock, [this] {
return stop || !tasks.empty();
});
if (stop && tasks.empty()) {
return;
}
task = std::move(tasks.front());
tasks.pop();
}
task();
}
});
}
}
~ThreadPool() {
{
std::lock_guard<std::mutex> lock(mtx);
stop = true;
}
cv.notify_all();
for (auto& t : workers) {
t.join();
}
}
template<class F>
void enqueue(F&& f) {
{
std::lock_guardstd::mutex lock(mtx);
tasks.emplace(std::forward<F>(f));
}
cv.notify_one();
}
};
int main() {
ThreadPool pool(4); // Criar 4 threads
for (int i = 0; i < 10; i++) {
pool.enqueue([i] {
std::cout << "Task " << i << " running" << std::endl;
});
}
return 0;
}
5. Prática: Ordenação Paralela
▶ Exemplo 1: Acelerando a ordenação com multithreading (Dificuldade ⭐⭐⭐⭐)
#include <iostream>
#include <vector>
#include <thread>
#include <algorithm>
void parallelSort(std::vector<int>& v, int left, int right) {
if (left >= right) return;
std::sort(v.begin() + left, v.begin() + right + 1);
}
int main() {
std::vector<int> v = {3, 1, 4, 1, 5, 9, 2, 6};
// Ordenar em paralelo com 2 threads
std::thread t1(parallelSort, std::ref(v), 0, v.size()/2);
std::thread t2(parallelSort, std::ref(v), v.size()/2 + 1, v.size()-1);
t1.join();
t2.join();
// Mesclar as duas metades ordenadas
std::inplace_merge(v.begin(), v.begin() + v.size()/2 + 1, v.end());
for (int x : v) {
std::cout << x << " ";
}
std::cout << std::endl;
return 0;
}
Saída:
3 1 4 1 5 9 2 6
▶ Exemplo 3: Protegendo um contador compartilhado com mutex (Dificuldade ⭐)
#include <iostream>
#include <thread>
#include <vector>
#include <mutex>
int counter = 0;
std::mutex mtx;
void increment() {
for (int i = 0; i < 1000; i++) {
std::lock_guard<std::mutex> lock(mtx);
counter++;
}
}
int main() {
std::vector<std::thread> threads;
for (int i = 0; i < 10; i++) {
threads.emplace_back(increment);
}
for (auto& t : threads) {
t.join();
}
std::cout << "counter = " << counter << std::endl;
return 0;
}
Saída:
counter = 10000
❓ Perguntas Frequentes
P: Multithreading é sempre mais rápido? R: Não necessariamente. Se as tarefas são muito pequenas, o overhead de criação e sincronização de threads pode superar o benefício.
P: Como depuro programas multithreaded? R: - Use
std::coutpara imprimir logs (mas adicione um mutex) - Use Valgrind, Helgrind, etc. para detectar condições de corrida - Use Thread Sanitizer (GCC/Clang)
P: O que há de novo no C++20? R: -
std::jthread(auto-join) - Semáforos (std::counting_semaphore) - Latches (std::latch) - Barriers (std::barrier)
📖 Resumo
| Ponto-Chave | Resumo |
|---|---|
| Deadlock | Quatro condições, três métodos de evitação |
| Lock de leitura-escrita | std::shared_mutex (C++17) |
| Operações atômicas | std::atomic, sem lock necessário |
| Pool de threads | Reutilizar threads, reduzir overhead |
| Algoritmos paralelos | C++17 std::execution |
📝 Exercícios
-
Básico (Dificuldade ⭐): Crie duas threads que cada uma incrementa uma variável global 100.000 vezes. Observe se o resultado está correto. Depois proteja com
std::mutexe teste novamente. -
Intermediário (Dificuldade ⭐⭐): Use
std::lock_guardpara proteger uma variável de "saldo da conta". Simule duas threads realizando operações de depósito/saque simultaneamente, garantindo que o saldo final esteja correto. -
Desafio (Dificuldade ⭐⭐⭐): Use
std::condition_variablepara implementar um modelo "produtor-consumidor". Uma thread produz dados e os coloca em uma fila, outra thread consome dados. O consumidor espera quando a fila está vazia.
- Condições de corrida: múltiplas threads acessam dados compartilhados simultaneamente
- std::mutex protege seções críticas
- std::lock_guard bloqueia e desbloqueia automaticamente (RAII)
- Deadlock: duas threads esperam uma pela outra liberar locks
- Variável de condição std::condition_variable notifica threads
Fase 6 Completa! Próxima: Fase 7 (Avançado e Prática)