← Hub Principal |
⚡

02: Lock-Free MPMC Queue (Michael-Scott)

Rust 2021 (Atomics & Concurrency)
Multithreading & Fearless Concurrency Módulo 02

Fila Concorrente Lock-Free (Algoritmo Michael-Scott)

Uma implementação de alto desempenho para sistemas paralelos onde múltiplas threads produzem e consomem dados simultaneamente sem sofrer bloqueios de Mutex, contenção no escalonador do SO (*context switches*) ou perigo de inversão de prioridades.


01. O Custo Oculto de Primitivas Bloqueantes (Mutex)

Em sistemas de alta vazão (*high-throughput*), primitivas como std::sync::Mutex delegam a resolução de contenção ao kernel do Sistema Operacional. Quando uma thread encontra um lock ocupado, o SO realiza uma troca de contexto (*context switch*), pausando a execução da thread e invalidando linhas de cache da CPU.

Vantagens das Estruturas Lock-Free:

  • Progresso Global Garantido: Pelo menos uma thread sempre avança a cada intervalo de tempo, eliminando deadlocks.
  • Imunidade a Interrupções: Se uma thread morrer ou sofrer preempção no meio da operação, as demais threads continuam operando normalmente.
  • Execução Direta no Hardware: As operações ocorrem em nível de registradores de CPU através de instruções de máquina como CMPXCHG (x86) ou LDREX/STREX (ARM).

02. Anatomia do Algoritmo Michael-Scott

Publicado em 1996 por Maged M. Michael e Michael L. Scott, este algoritmo resolve o principal problema de filas concorrentes: como enfileirar e desenfileirar simultaneamente sem que as duas operações entrem em conflito quando há poucos elementos?

1. O Nó Sentinela (Dummy Node)

A fila nunca fica estritamente com zero nós físicos. Ela sempre contém um nó artificial sentinela. O head sempre aponta para o sentinela, e o primeiro dado real está em head.next. Isso desacopla completamente os ponteiros de cabeça e cauda, permitindo que um produtor e um consumidor operem ao mesmo tempo sem tocar na mesma variável atômica.

2. Cooperação Entre Threads

A inserção ocorre em dois passos atômicos: primeiro liga o novo nó em tail.next, e depois avança tail. Se uma thread for interrompida entre esses dois passos, qualquer outra thread que tentar inserir detecta que a cauda está atrasada e a move para a frente cooperativamente antes de prosseguir.

03. Modelo de Consistência de Memória (Memory Ordering)

Em Rust, operações atômicas não usam apenas a ordem sequencial estrita (Ordering::SeqCst), que impõe barreiras de memória pesadas na CPU. Utilizamos a semântica de Acquire-Release:

Atomic Ordering Comportamento de Hardware Uso na Fila
Ordering::Acquire Impede que leituras e escritas subsequentes sejam reordenadas para antes deste ponto na CPU. Ao carregar ponteiros de head, tail e next.
Ordering::Release Garante que todas as escritas anteriores estejam visíveis para outras threads que fizerem Acquire. No compare_exchange que publica o novo nó ou atualiza a cauda.
Ordering::Relaxed Garante apenas atomicidade do valor, sem impor barreiras de reordenação entre instruções adjacentes. Em casos de falha do CAS onde o loop recomeça imediatamente.

04. O Problema ABA & Reclamação Segura de Memória (SMR)

⚠️ Consideração de Engenharia de Produção:

Em estruturas baseadas em CAS que desalocam memória no Heap, existe o risco do Problema ABA: uma thread lê o endereço 0x1000, outra thread desenfileira esse nó, o desaloca, aloca outro objeto no mesmo endereço 0x1000 e a primeira thread conclui o CAS achando que nada mudou.

Em bibliotecas de produção em Rust (como crossbeam), essa questão é resolvida com técnicas de Epoch-Based Memory Reclamation (EBR) ou Hazard Pointers, que adiam a liberação da memória até que nenhuma thread concorrente mantenha referências ativas àquele nó.

cargo run --bin lock_free_queue
Ambiente pronto. Clique em "Compilar & Executar" para disparar threads concorrentes em Rust.