Sobes.tech
Senior

Está familiarizado com o conceito de lock-free?

sobes.tech IA

Resposta da IA

Sim, conheço. Os algoritmos sem bloqueio (lock-free) são métodos de desenvolvimento de programas paralelos que garantem o progresso do sistema como um todo, mesmo que alguns threads estejam suspensos. Conseguem isso sem usar primitivas tradicionais de sincronização, como mutexes ou semáforos, que podem levar a bloqueios de threads. Em vez disso, usam operações atómicas.

Conceitos principais:

  • Operações atómicas: Operações que são executadas na íntegra e não podem ser interrompidas ou entrelaçadas com outras operações. Em Golang, as operações atómicas estão disponíveis no pacote sync/atomic (por exemplo, AddInt64, CompareAndSwapPointer).
  • Progresso: Este é um aspeto chave dos algoritmos lock-free. Distinguem-se níveis de progresso:
    • Obstruction-Free (livre de obstáculos): Se um thread for executado isoladamente, concluirá sua operação em um número finito de passos. A presença de outros threads pode causar deadlocks.
    • Lock-Free (não bloqueante): Garante que pelo menos um thread que tente realizar uma operação terá sucesso em um número finito de passos, mesmo que outros threads estejam suspensos. No entanto, um thread específico pode ser "starved".
    • Wait-Free (sem espera): O nível mais forte. Garante que cada thread que tente realizar uma operação a concluirá em um número finito de passos, independentemente da velocidade ou suspensão de outros threads. Exclui a starvation.
  • CAS (Compare-And-Swap): Operação atómica chave. Compara o valor atual de uma variável com um valor esperado e, se coincidirem, substitui-o de forma atómica por um novo valor. Retorna um valor booleano que indica se a substituição foi bem-sucedida. Permite implementar ciclos read-modify-write sem bloqueios.

Exemplo de uso de sync/atomic para um contador lock-free:

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"
)

func main() {
	var counter int64 // Variável a ser acedida de forma atómica

	var wg sync.WaitGroup
	for i := 0; i < 1000; i++ {
		wg.Add(1)
		go func() {
			defer wg.Done()
			// Incremento atómico do contador em 1
			atomic.AddInt64(&counter, 1)
		}()
	}

	wg.Wait()

	fmt.Println("Valor final do contador:", atomic.LoadInt64(&counter)) // Leitura atómica do valor
}

Vantagens do lock-free:

  • Ausência de deadlocks: Como não há primitivas de bloqueio, deadlocks são impossíveis.
  • Resistência a atrasos nos threads: Se um thread for suspenso (por exemplo, pelo agendador), isso não bloqueia outros threads que realizam operações sobre os mesmos dados, ao contrário dos mutexes.
  • Potencialmente melhor desempenho: Em certos cenários, especialmente com alta concorrência e seções críticas curtas, lock-free pode ser mais rápido, pois evita custos de bloqueio e desbloqueio.

Desvantagens do lock-free:

  • Complexidade de implementação: Desenvolver algoritmos lock-free é muito mais complexo do que usar bloqueios tradicionais. É mais difícil raciocinar sobre a correção e evitar erros.
  • Problema ABA: Um problema conhecido onde o valor de uma variável pode ser alterado de A para B e depois de volta para A. CAS pode pensar que nada mudou. Técnicas especiais (como double CAS ou adição de versões) são necessárias para resolvê-lo.
  • Carga na memória e cache: Operações atómicas podem exigir sincronização mais frequente das caches dos processadores.
  • Nem sempre mais rápido: Em cenários com baixa concorrência ou seções críticas longas, bloqueios tradicionais podem ser mais eficientes.

Em Golang, abordagens lock-free são usadas na implementação de estruturas internas (por exemplo, alguns aspetos do scheduler, canais), e também podem ser usadas por desenvolvedores para otimizar trechos de código altamente concorrentes usando o pacote sync/atomic. No entanto, na maioria dos casos, as primitivas de sincronização padrão do pacote sync (mutexes, WaitGroup, Cond, RWMutex) são suficientes e mais simples de usar. A aplicação de lock-free requer uma compreensão profunda das operações atómicas e das características da arquitetura multiprocessador.

Tabela comparativa Lock-Based vs Lock-Free:

Característica Lock-Based (com bloqueios) Lock-Free (não bloqueante)
Primitivas Mutexes, semáforos Operações atómicas (CAS, Add, Load, Store)
Progresso do sistema Pode bloquear-se Garante progresso (um ou mais threads)
Interbloqueios Possíveis Impossíveis
Resistência a pausas Baixa (parar um thread bloqueia os outros) Alta (parar um thread não bloqueia os outros)
Complexidade de implementação Relativamente simples Alta
Problema de starvation Possível com primitivas com fila injusta Possível em lock-free, mas não wait-free
Problema ABA Ausente Possível
Aplicação típica A maioria das tarefas paralelas Estruturas de dados altamente concorrentes, otimizações de baixo nível