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.

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.