1. Por que precisamos de std::condition_variable se já existe mutex
Se você olhar para o mutex com olhos de iniciante, pode parecer: “mas eu já posso proteger a fila de tarefas, então está tudo bem”. De fato, o mutex protege bem os dados contra races. Mas há um problema: ele não sabe “esperar de forma elegante”. Se o consumidor não tem trabalho, ele ou fica em loop (busy-wait), ou periodicamente dorme com sleep_for e “espia” se apareceu trabalho. Ambos os métodos são um compromisso entre consumo de CPU e latência.
std::condition_variable é como uma campainha na porta: você não fica em cima do tapete checando a cada 10 milissegundos se o entregador chegou. Você pediu para “esperar” e… realmente espera. Quando alguém trouxer a pizza (a tarefa) — vão tocar pra você e você continuará.
Três participantes do protocolo: estado, mutex e condition_variable
Agora vem uma ideia importante que deve ser repetida como um mantra em multithreading: nós esperamos pelo estado, não pelo “sinal”. O estado é algo que pode ser verificado “agora mesmo”: a flag ready, o contador tokens, a fila queue (vazia/não vazia). E a notificação (notify_*) é apenas uma dica educada: “verifique novamente”.
A combinação é assim: temos um estado protegido (por exemplo, a fila de tarefas), que só lemos e modificamos sob um único mutex. Ao lado existe a condition_variable, que permite a uma thread “adormecer” até que o estado possa ficar adequado.
Esquema simples do protocolo:
flowchart TD
A[Consumer adquiriu mutex] --> B{Estado pronto?}
B -- não --> C["cv.wait(lock, predicate) mutex temporariamente liberado, thread dorme"]
C --> D[Despertar: mutex novamente adquirido]
D --> B
B -- sim --> E[Pegamos os dados e saímos da seção crítica]
Exatamente esse “loop de verificação” está embutido em wait(lock, predicate).
2. Por que wait funciona com std::unique_lock, e não com std::lock_guard
À primeira vista isso pode irritar: “por que não usar, como sempre, lock_guard?” Porque wait faz um truque que lock_guard não consegue: libera temporariamente o mutex e depois o readquire. A thread precisa dormir, mas não pode manter o mutex bloqueado, senão outras threads não poderão mudar o estado e despertá-la.
Veja a ideia do unique_lock: ele não é apenas “adquiriu e segura até o fim do scope”, ele também sabe fazer unlock() e lock().
#include <mutex>
std::mutex m;
void unique_lock_demo() {
std::unique_lock<std::mutex> lock(m);
lock.unlock(); // liberou o mutex
lock.lock(); // readquiriu novamente
}
E agora uma comparação em tabela:
| Ferramenta | O que faz | É possível chamar manualmente unlock()? | Serve para cv.wait()? |
|---|---|---|---|
|
“adquire → segura até o fim do scope” | não | não |
|
“adquire → posso liberar → posso readquirir” | sim | sim |
E é por essa “gerenciabilidade” que unique_lock é o parceiro padrão do condition_variable.
4. Assinatura e sentido de wait(lock, predicate)
Aqui está a parte central da aula: o padrão correto de espera. Na biblioteca padrão existe uma sobrecarga de wait que aceita um predicado (uma função booleana). Essa sobrecarga existe justamente para que a espera seja correta e robusta. Nos materiais do padrão aparece separadamente o tema “condition_variable::wait com predicado” (inclusive como assunto de formulação de requisitos).
O wait(lock) “puro” — espera sem lógica
Se você escrever assim:
cv.wait(lock);
você, na prática, está dizendo: “me coloque para dormir e acorde quando quiserem”. Isso às vezes é aplicável em protocolos muito de baixo nível, mas para código didático e aplicativo é quase sempre perigoso: ao acordar você precisa verificar por conta própria se pode continuar.
wait(lock, predicate) — espera com verificação correta
A forma correta é assim:
cv.wait(lock, [&] { return /* condição de prontidão */; });
E o sentido é: “espere até que a condição seja verdadeira”. Internamente a biblioteca faz o equivalente a um loop “checou → se não, dormiu → acordou → checou de novo”.
Equivalente clássico:
while (!predicate()) {
cv.wait(lock);
}
E por isso o predicado não deve ter efeitos colaterais: ele pode ser chamado muitas vezes.
5. Mini-exemplo: esperamos corretamente a flag ready
Quando se começa a escrever código multithreaded, é comum testar algo simples: há uma flag “pronto/não pronto”, o producer define true, o consumer espera.
#include <condition_variable>
#include <mutex>
std::mutex m;
std::condition_variable cv;
bool ready = false;
void consumer_waits() {
std::unique_lock<std::mutex> lock(m);
cv.wait(lock, [] { return ready; });
// aqui ready == true, e o mutex está novamente bloqueado
}
O producer, por sua vez, deve mudar o estado sob o mesmo mutex:
#include <condition_variable>
#include <mutex>
extern std::mutex m;
extern std::condition_variable cv;
extern bool ready;
void producer_sets_ready() {
{
std::lock_guard<std::mutex> lock(m);
ready = true;
}
cv.notify_one();
}
Repare na forma: alteramos ready dentro do bloco para que o mutex seja garantidamente liberado, e só então chamamos notify_one().
6. Inserindo wait(lock, predicate) em uma mini-aplicação
Imagine que na aula anterior (sobre busy-wait) já começamos a escrever um “processador de tarefas de console”: uma thread adiciona tarefas, outra as executa. Até então o consumer podia “girar” e checar queue.empty(). Agora faremos direito: o consumer vai dormir enquanto a fila estiver vazia.
Para simplificar, a tarefa será apenas um número int (por exemplo, “quantas vezes imprimir uma linha” ou “qual número calcular”).
Estado da aplicação: fila + sincronização.
#include <condition_variable>
#include <mutex>
#include <queue>
std::mutex g_mutex;
std::condition_variable g_cv;
std::queue<int> g_tasks;
Producer: coloca a tarefa e acorda o consumer
O producer é, por exemplo, o main, que lê comandos do usuário.
#include <condition_variable>
#include <mutex>
#include <queue>
extern std::mutex g_mutex;
extern std::condition_variable g_cv;
extern std::queue<int> g_tasks;
void push_task(int x) {
{
std::lock_guard<std::mutex> lock(g_mutex);
g_tasks.push(x);
}
g_cv.notify_one();
}
Consumer: esperamos enquanto a fila estiver vazia
Aqui vive nosso wait(lock, predicate):
#include <condition_variable>
#include <mutex>
#include <queue>
extern std::mutex g_mutex;
extern std::condition_variable g_cv;
extern std::queue<int> g_tasks;
int pop_task_blocking() {
std::unique_lock<std::mutex> lock(g_mutex);
g_cv.wait(lock, [] { return !g_tasks.empty(); });
int x = g_tasks.front();
g_tasks.pop();
return x;
}
Note como isso é “humano”: literalmente escrevemos “espere até que a fila não esteja vazia”. E não é mágica: wait libera o mutex, dorme, e depois, ao acordar, readquire e recheca o predicado.
7. Como capturar o estado no predicado: [&], [], [this]
Quando você escreve cv.wait(lock, ...), normalmente usa uma lambda. Surge então a pergunta: “como capturar variáveis?”. Isso não é só cosmético — afeta correção e legibilidade.
Se o estado for variáveis globais (como em nossos exemplos curtos), pode usar [], pois o acesso será às variáveis globais:
g_cv.wait(lock, [] { return !g_tasks.empty(); });
Se você encapsular a fila em um objeto (como faremos adiante), então geralmente teremos this->tasks_, this->closed_, e aí é conveniente capturar this implicitamente com [&] ou explicitamente com [this]. Em código didático costuma ficar mais legível usar [&]:
cv_.wait(lock, [&] { return !tasks_.empty(); });
Hábito importante: o predicado deve ser uma “checagem pura” do estado, sem push, sem pop, sem imprimir no console. Ele pode ser chamado frequentemente, e você não quer que seu código imprima “acordei!” 200 vezes apenas porque a thread foi acordada fora de hora.
8. notify_one(), notify_all() e onde chamar notify_*
Neste ponto é importante não se perder nos detalhes, mas é preciso fixar a intuição básica. Existem duas maneiras de acordar quem espera:
notify_one() acorda uma thread esperando. É ideal para “uma nova tarefa → um worker é acordado e a pega”.
notify_all() acorda todas as threads esperando. É útil quando o estado mudou de forma que potencialmente muitas threads podem continuar (ou quando é preciso avisar todos sobre uma mudança importante). Veremos casos de término correto e quando notify_all() é necessário.
Nos textos sobre a biblioteca padrão observa-se que se dá atenção à eficiência e às definições corretas de comportamento em torno de condition_variable.
Agora a clássica dúvida: “posso chamar notify_one() enquanto o mutex ainda está bloqueado?”. Tecnicamente sim, mas na prática costuma ser mais simples e mais claro fazer assim: primeiro altere o estado sob o mutex, saia do escopo (libere o mutex) e só então notifique.
A razão é prática. Se você chamar notify_one() com o mutex bloqueado, a thread despertada tentará quase imediatamente readquirir o mutex, mas não conseguirá porque você ainda o segura. Formalmente não é erro, mas parece com “toquei a campainha, mas deixei a porta trancada”.
Bom padrão didático:
{
std::lock_guard<std::mutex> lock(m);
ready = true;
}
cv.notify_one();
Esse código é mais fácil de ler e manter: a mudança de estado e o sinal sobre ela ficam visivelmente separados.
9. Mais alguns mini-exemplos de wait(lock, predicate) para consolidar
Às vezes vale ver o mesmo padrão em diferentes cenários. É como aprender gramática de um idioma: até ver 10 sentenças você acha que entendeu, mas pode não ser assim.
“Tokens”: esperamos até que o contador fique maior que zero
#include <condition_variable>
#include <mutex>
std::mutex m;
std::condition_variable cv;
int tokens = 0;
void take_token() {
std::unique_lock<std::mutex> lock(m);
cv.wait(lock, [] { return tokens > 0; });
--tokens;
}
“Tem trabalho”: esperamos até que exista pelo menos um elemento
#include <condition_variable>
#include <mutex>
#include <queue>
std::mutex m;
std::condition_variable cv;
std::queue<int> q;
int pop_one() {
std::unique_lock<std::mutex> lock(m);
cv.wait(lock, [] { return !q.empty(); });
int x = q.front();
q.pop();
return x;
}
“Checagem de prontidão” e impressão no console
#include <iostream>
#include <condition_variable>
#include <mutex>
std::mutex m;
std::condition_variable cv;
bool ready = false;
void wait_and_print() {
std::unique_lock<std::mutex> lock(m);
cv.wait(lock, [] { return ready; });
std::cout << "Ready!\n"; // Ready!
}
10. Erros típicos ao trabalhar com wait(lock, predicate)
Erro nº1: usar cv.wait(lock) “porque é mais curto”.
Realmente é mais curto, mas é como escrever if (x) ... sem entender o que é x e quem o modifica. Se você espera por uma condição específica (fila não vazia, flag pronta, tokens disponíveis), o estilo correto básico é wait(lock, predicate). Ele liga a espera ao estado, não a um “fato místico de ter sido acordado”.
Erro nº2: tentar passar std::lock_guard para wait.
lock_guard não sabe liberar o mutex, e wait precisa liberar o mutex durante o sono. Por isso para espera você precisa de std::unique_lock. Se o compilador reclama, provavelmente você escolheu o tipo de lock errado.
Erro nº3: predicado com efeitos colaterais.
Algumas vezes iniciantes escrevem um predicado que imprime no console, incrementa um contador ou até tenta pop() da fila. Isso é uma ótima receita para caos: o predicado pode ser chamado muitas vezes, inclusive quando não há trabalho real. O predicado deve ser uma checagem pura de bool, nada mais.
Erro nº4: modificar o estado sem mutex e esperar que notify_one() “vá sincronizar as coisas”.
notify_one() não protege dados nem substitui o mutex. Ele apenas acorda uma thread. Se a escrita em ready ou o queue.push for feita sem o mesmo mutex, você quebra a disciplina de acesso ao estado compartilhado. No melhor caso terá bugs raros e estranhos, no pior caso espera infinita ou crashes.
Erro nº5: manter o mutex durante um trabalho longo após acordar.
A seção crítica correta costuma ser: “cheque a condição → pegue os dados → atualize a estrutura → libere o mutex”. Se, após wait, você começa a fazer computação pesada, imprimir gigabytes de texto ou simular trabalho com sleep_for com o mutex ainda bloqueado, você transforma todo o código multithread em sequencial, com stress extra.
GO TO FULL VERSION