Módulo 8 - Concorrência com threads
Fila e o padrão produtor-consumidor
10 min de leitura · por Cesar Gargiulo, revisado pela equipe ValorFinal e GuardiaSec · Atualizado em 01/07/2026
O que você vai aprender
- Entender o padrão produtor-consumidor.
- Usar queue.Queue para trocar itens entre threads com segurança.
- Aplicar put, get, task_done e join da fila.
- Encerrar consumidores com um sentinela de fim.
Ouvir o resumo desta aula
Um recap de cerca de 2 minutos na voz do Valim, para ouvir no trânsito ou na academia.
Ler a transcrição do resumo
Resumo da aula: Fila e o padrão produtor-consumidor.
Os objetivos desta aula. Entender o padrão produtor-consumidor. Usar queue.Queue para trocar itens entre threads com segurança. Aplicar put, get, task_done e join da fila. Encerrar consumidores com um sentinela de fim.
Veja o essencial, parte por parte.
O padrão produtor-consumidor. No padrão produtor-consumidor, uma parte gera tarefas e outra as processa, ligadas por uma fila.
put, get e o básico da fila. A interface da fila é enxuta.
Encerrando com sentinela e escalando. A fila já é thread-safe: você não escreve nenhuma trava para inserir ou retirar itens.
Esse foi o resumo do essencial. Para se aprofundar, leia a aula completa e responda os exercícios.
O padrão produtor-consumidor
Muitos problemas de concorrência têm o mesmo formato: um lado gera trabalho e outro executa, em ritmos diferentes. Um site que recebe pedidos e uma fila de processamento que os atende; um leitor que baixa páginas e vários trabalhadores que as analisam. Esse é o padrão produtor-consumidor. A peça que une os dois é uma fila: o produtor deposita itens nela e segue produzindo; o consumidor retira itens e os processa no seu tempo. A fila desacopla os dois ritmos. Se o produtor é rápido e o consumidor lento, os itens se acumulam na fila em vez de se perder; se o consumidor está livre, ele espera pelo próximo item sem gastar processador à toa.
Você poderia montar isso com uma lista comum e um Lock, mas seria fácil errar. A boa notícia é que a biblioteca padrão já traz a estrutura pronta e segura: a queue.Queue. Ela foi feita para uso entre threads, então várias podem inserir e retirar ao mesmo tempo sem nenhuma race condition, porque a própria fila faz a sincronização por dentro. Você não precisa de Lock nenhum para trocar itens; a fila é a coordenação. Isso é um exemplo do princípio de preferir estruturas seguras por construção a controlar travas na mão.
put, get e o básico da fila
A interface da fila é enxuta. O produtor chama put para colocar um item. O consumidor chama get para retirar o próximo; se a fila estiver vazia, o get bloqueia e espera até chegar algo, o que evita ficar checando a fila em laço. Quando o consumidor termina de processar um item, ele chama task_done para avisar a fila. E quem coordena tudo pode chamar join na fila, que bloqueia até todos os itens colocados terem sido processados, ou seja, até haver um task_done para cada put. Esse trio, get, task_done e join, deixa você esperar todo o trabalho terminar sem controlar contadores na mão.
import queue
import threading
import time
fila = queue.Queue()
def trabalhador():
while True:
item = fila.get() # espera ate ter item
if item is None: # sentinela de fim
fila.task_done()
break
print(f"processando {item}")
time.sleep(0.5)
fila.task_done() # avisa que terminou este item
# um consumidor
t = threading.Thread(target=trabalhador)
t.start()
# produtor coloca 5 tarefas
for n in range(5):
fila.put(n)
fila.put(None) # sentinela avisa o consumidor para parar
fila.join() # espera tudo ser processado
t.join()
print("Fila esvaziada")put insere, get retira e bloqueia se vazia, task_done marca fim de item, join espera tudo.
Encerrando com sentinela e escalando
Um consumidor que roda em while True nunca para sozinho, porque o get simplesmente espera pelo próximo item para sempre. O jeito limpo de encerrar é o sentinela: um valor combinado que significa acabou o trabalho. No exemplo, esse valor é None. Quando o trabalhador retira um None da fila, ele sabe que não vem mais nada e sai do laço. Com vários consumidores, você coloca um sentinela para cada um, garantindo que todos recebam o aviso de parada. É simples e previsível, sem precisar interromper threads à força, o que seria arriscado.
import queue
import threading
fila = queue.Queue()
N_TRABALHADORES = 3
def trabalhador(id_):
while True:
item = fila.get()
if item is None:
fila.task_done()
break
print(f"trab {id_} fez {item}")
fila.task_done()
threads = [threading.Thread(target=trabalhador, args=(i,))
for i in range(N_TRABALHADORES)]
for t in threads:
t.start()
for tarefa in range(10):
fila.put(tarefa)
for _ in range(N_TRABALHADORES): # um sentinela por consumidor
fila.put(None)
fila.join()
for t in threads:
t.join()
print("Todos os trabalhadores encerraram")Vários consumidores dividem a fila; um sentinela None por consumidor encerra todos.
Teste rápido
Para que serve colocar um valor sentinela (como None) na queue.Queue?
Perguntas frequentes
- Preciso de Lock para usar a queue.Queue?
- Não. A queue.Queue já é thread-safe por dentro: várias threads podem chamar put e get ao mesmo tempo sem race condition. Ela cuida da sincronização para você. É justamente essa segurança embutida que a torna preferível a montar uma fila com lista e Lock na mão.
- O que o get faz se a fila estiver vazia?
- Por padrão, ele bloqueia e espera até chegar um item, o que é ótimo, pois o consumidor dorme sem gastar processador. Se você não quiser bloquear, pode usar get com timeout ou get_nowait, que lança uma exceção quando a fila está vazia em vez de esperar.
- Para que serve o task_done junto com o join da fila?
- O join da fila bloqueia até que cada item colocado com put tenha um task_done correspondente. Assim você espera todo o trabalho ser processado sem contar itens na mão. É o consumidor que chama task_done ao terminar cada item, fechando o par com o put.
- Posso ter vários produtores e vários consumidores?
- Pode, e é comum. A mesma fila aceita vários produtores inserindo e vários consumidores retirando ao mesmo tempo, tudo com segurança. Ao encerrar, lembre de colocar um sentinela para cada consumidor, para que todos recebam o aviso de parada e saiam do laço.
- Qual a diferença entre queue.Queue e a coleção deque?
- A collections.deque é uma fila de duas pontas muito eficiente, mas não foi feita com a sincronização entre threads em mente para o padrão produtor-consumidor. A queue.Queue oferece a interface bloqueante e o task_done pensados para threads. Para coordenar threads, use a queue.Queue.
Fontes
Seu progresso fica salvo neste aparelho. Assinantes sincronizam entre os aparelhos.