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:

  1. Exclusão mútua: Recursos não podem ser compartilhados
  2. Posse e espera: Uma thread possuindo recursos espera por outros recursos
  3. Sem preempção: Recursos não podem ser liberados à força
  4. 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 ⭐)

TEXT 📖 Somente leitura
#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:

TEXT 📖 Somente leitura
Thread 1 done
Thread 2 done

💡 Dica:


(3) 1.3 Evitando Deadlocks

Método 1: Ordem de Bloqueio Fixa

CPP
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

TEXT 📖 Somente leitura
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)

CPP
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 ⭐⭐⭐)

CPP
#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:

TEXT 📖 Somente leitura
(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 ⭐⭐)

CPP
#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:



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 ⭐⭐⭐⭐)

TEXT 📖 Somente leitura
#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 ⭐⭐⭐⭐)

CPP
#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;
}
▶ Experimente

Saída:

TEXT 📖 Somente leitura
3 1 4 1 5 9 2 6

▶ Exemplo 3: Protegendo um contador compartilhado com mutex (Dificuldade ⭐)

CPP
#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;
}
▶ Experimente

Saída:

TEXT 📖 Somente leitura
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::cout para 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

  1. 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::mutex e teste novamente.

  2. Intermediário (Dificuldade ⭐⭐): Use std::lock_guard para 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.

  3. Desafio (Dificuldade ⭐⭐⭐): Use std::condition_variable para 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.



Fase 6 Completa! Próxima: Fase 7 (Avançado e Prática)

Web-Tutorial.com

Equipe Técnica Web-Tutorial

Uma plataforma de tutoriais mantida por diversos desenvolvedores. Cada tutorial é escrito e revisado por profissionais da área correspondente. Trabalhamos para manter nosso conteúdo preciso e confiável — se encontrar algum problema, avise-nos.

100%