CodeGym /Cursos /C++ SELF /std::condition_variable: wait(lock, predicate)

std::condition_variable: wait(lock, predicate)

C++ SELF
Nível 70, Lição 1
Disponível

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()?
std::lock_guard
“adquire → segura até o fim do scope” não não
std::unique_lock
“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.

1
Tarefa
C++ SELF, nível 70, lição 1
Bloqueado
Iniciar com Enter
Iniciar com Enter
1
Tarefa
C++ SELF, nível 70, lição 1
Bloqueado
Largada coletiva
Largada coletiva
1
Tarefa
C++ SELF, nível 70, lição 1
Bloqueado
Quadrados da fila
Quadrados da fila
1
Tarefa
C++ SELF, nível 70, lição 1
Bloqueado
Pool de tokens
Pool de tokens
Comentários
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION