Atomic operations com sync/atomic
1. Introdução às Operações Atômicas em Go
Operações atômicas são instruções de máquina que executam uma operação de leitura-modificação-escrita de forma indivisível. Em um ambiente concorrente, múltiplas goroutines podem acessar a mesma variável simultaneamente, resultando em race conditions. As operações atômicas garantem que nenhuma outra goroutine observe a operação pela metade.
A principal diferença entre operações atômicas e locks (como sync.Mutex) está no nível de granularidade e overhead. Locks suspendem a execução de goroutines quando há contenção, enquanto operações atômicas são implementadas diretamente no nível de hardware usando instruções como LOCK CMPXCHG (x86) ou LDREX/STREX (ARM). Isso torna as operações atômicas significativamente mais leves para operações simples.
O pacote sync/atomic fornece operações atômicas de baixo nível para tipos primitivos (int32, int64, uint32, uint64, uintptr, unsafe.Pointer) e, a partir do Go 1.19, também oferece atomic.Int32, atomic.Int64, atomic.Uint64 e outros tipos convenientes.
2. Operações Básicas: Load, Store e Swap
As operações mais fundamentais são Load (leitura atômica) e Store (escrita atômica). Sem elas, leituras e escritas concorrentes podem resultar em dados corrompidos.
package main
import (
"fmt"
"sync/atomic"
"time"
)
type RequestCounter struct {
count atomic.Int64
}
func (rc *RequestCounter) Increment() {
rc.count.Add(1)
}
func (rc *RequestCounter) Value() int64 {
return rc.count.Load()
}
func main() {
counter := &RequestCounter{}
for i := 0; i < 100; i++ {
go func() {
for j := 0; j < 1000; j++ {
counter.Increment()
}
}()
}
time.Sleep(time.Second)
fmt.Printf("Total requests: %d\n", counter.Value())
}
A operação Swap permite trocar um valor atômico e obter o valor anterior:
var value atomic.Int64
old := value.Swap(42) // Troca o valor atual por 42 e retorna o anterior
3. Operações de Comparação e Troca (CAS - Compare And Swap)
CAS é a operação atômica mais versátil. Ela compara o valor atual com um esperado e, se forem iguais, substitui por um novo valor. Tudo isso ocorre atomicamente.
type SpinLock struct {
locked atomic.Bool
}
func (s *SpinLock) Lock() {
for s.locked.Load() {
// Spin-wait
}
if !s.locked.CompareAndSwap(false, true) {
s.Lock() // Tenta novamente
}
}
func (s *SpinLock) Unlock() {
s.locked.Store(false)
}
O problema ABA: Considere um ponteiro que muda de A para B e depois volta para A. Uma operação CAS pode não detectar essa mudança. Para resolver isso, usa-se um contador de versão junto com o valor:
type ABAFreeValue struct {
value atomic.Value
version atomic.Uint64
}
func (v *ABAFreeValue) CompareAndSwap(old, new interface{}) bool {
currentVersion := v.version.Load()
if v.value.Load() != old {
return false
}
return v.version.CompareAndSwap(currentVersion, currentVersion+1)
}
4. Operações Aritméticas Atômicas: Add e Sub
Add é a operação mais comum para contadores e métricas:
type ServerMetrics struct {
requests atomic.Int64
errors atomic.Int64
bytesSent atomic.Int64
}
func (m *ServerMetrics) RecordRequest() {
m.requests.Add(1)
}
func (m *ServerMetrics) RecordError() {
m.errors.Add(1)
}
func (m *ServerMetrics) RecordBytes(n int64) {
m.bytesSent.Add(n)
}
func (m *ServerMetrics) Snapshot() map[string]int64 {
return map[string]int64{
"requests": m.requests.Load(),
"errors": m.errors.Load(),
"bytes_sent": m.bytesSent.Load(),
}
}
Para tipos unsigned, use AddUint32 ou AddUint64:
var counter atomic.Uint64
counter.Add(1) // Incremento seguro para uint64
5. Trabalhando com Tipos Genéricos: atomic.Value
atomic.Value permite armazenar qualquer tipo de forma atômica, mas requer type assertion na leitura:
type AppConfig struct {
MaxConnections int
Timeout time.Duration
DebugMode bool
}
type ConfigManager struct {
config atomic.Value
}
func NewConfigManager(initial AppConfig) *ConfigManager {
cm := &ConfigManager{}
cm.config.Store(initial)
return cm
}
func (cm *ConfigManager) Get() AppConfig {
return cm.config.Load().(AppConfig)
}
func (cm *ConfigManager) Update(newConfig AppConfig) {
cm.config.Store(newConfig)
}
// Uso em runtime
func main() {
cm := NewConfigManager(AppConfig{
MaxConnections: 100,
Timeout: 30 * time.Second,
DebugMode: false,
})
// Goroutine que atualiza configurações
go func() {
for {
time.Sleep(5 * time.Second)
cm.Update(AppConfig{
MaxConnections: 200,
Timeout: 60 * time.Second,
DebugMode: true,
})
}
}()
// Leitura segura de qualquer goroutine
config := cm.Get()
fmt.Printf("Max connections: %d\n", config.MaxConnections)
}
Importante: atomic.Value não deve ser copiada após o primeiro uso, e o tipo armazenado deve ser consistente (não misture tipos diferentes no mesmo Value).
6. Padrões Avançados com sync/atomic
Contador de Referência Atômico
type RefCounted struct {
refs atomic.Int32
data interface{}
}
func (r *RefCounted) Retain() {
r.refs.Add(1)
}
func (r *RefCounted) Release() {
if r.refs.Add(-1) == 0 {
// Cleanup
r.data = nil
}
}
Fila Lock-Free Simples (SPSC - Single Producer Single Consumer)
type LockFreeQueue struct {
buffer []interface{}
head atomic.Uint64
tail atomic.Uint64
size uint64
}
func NewLockFreeQueue(size uint64) *LockFreeQueue {
return &LockFreeQueue{
buffer: make([]interface{}, size),
size: size,
}
}
func (q *LockFreeQueue) Enqueue(item interface{}) bool {
for {
tail := q.tail.Load()
nextTail := (tail + 1) % q.size
if nextTail == q.head.Load() {
return false // Fila cheia
}
if q.tail.CompareAndSwap(tail, nextTail) {
q.buffer[tail] = item
return true
}
}
}
func (q *LockFreeQueue) Dequeue() (interface{}, bool) {
for {
head := q.head.Load()
if head == q.tail.Load() {
return nil, false // Fila vazia
}
item := q.buffer[head]
if q.head.CompareAndSwap(head, (head+1)%q.size) {
return item, true
}
}
}
7. Boas Práticas e Erros Comuns
Quando usar cada abordagem:
- Use sync/atomic para operações simples em tipos primitivos (contadores, flags, ponteiros)
- Use sync.Mutex para seções críticas que envolvem múltiplas operações ou estruturas complexas
- Use channels para comunicação entre goroutines (não apenas sincronização)
Alinhamento de memória: Em arquiteturas 32-bit, variáveis de 64-bit (int64, uint64) precisam estar alinhadas a 8 bytes. Em structs, organize os campos para evitar problemas:
// Correto: campos de 64-bit primeiro
type Aligned struct {
counter int64 // 8 bytes
active bool // 1 byte
_ [7]byte // padding
}
// Incorreto: pode causar panic em 32-bit
type Misaligned struct {
active bool
counter int64 // Não alinhado a 8 bytes
}
Ponteiros e GC: atomic.StorePointer e atomic.LoadPointer trabalham com unsafe.Pointer. O garbage collector pode mover objetos, mas esses ponteiros são tratados corretamente pelo runtime. Use atomic.Value sempre que possível para evitar manipulação direta de ponteiros.
8. Benchmark e Performance: atomic vs Locks
package benchmark
import (
"sync"
"sync/atomic"
"testing"
)
type AtomicCounter struct {
value atomic.Int64
}
type MutexCounter struct {
mu sync.Mutex
value int64
}
func BenchmarkAtomicAdd(b *testing.B) {
c := &AtomicCounter{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
c.value.Add(1)
}
})
}
func BenchmarkMutexAdd(b *testing.B) {
c := &MutexCounter{}
b.RunParallel(func(pb *testing.PB) {
for pb.Next() {
c.mu.Lock()
c.value++
c.mu.Unlock()
}
})
}
Execute com:
go test -bench=. -benchmem -race
Em cenários de baixa contenção, atomic.Add é 5-10x mais rápido que Mutex. Em alta contenção (muitas goroutines competindo), a diferença diminui, mas operações atômicas ainda são mais eficientes por não causar trocas de contexto no scheduler.
Resultados típicos (Go 1.21, 8 cores):
BenchmarkAtomicAdd-8 100000000 12.3 ns/op 0 B/op 0 allocs/op
BenchmarkMutexAdd-8 30000000 45.7 ns/op 0 B/op 0 allocs/op
Referências
- Documentação oficial do pacote sync/atomic — Referência completa de todas as funções e tipos do pacote, incluindo as APIs modernas introduzidas no Go 1.19
- Go Memory Model — Documentação oficial sobre o modelo de memória do Go, essencial para entender garantias de visibilidade em operações atômicas
- Lock-Free Programming in Go — Artigo técnico detalhado sobre padrões lock-free usando sync/atomic em Go
- Go Concurrency Patterns: Pipelines and cancellation — Post oficial do blog Go sobre padrões de concorrência, incluindo uso de operações atômicas
- Understanding Atomic Operations in Go — Tutorial prático com exemplos de código e explicações sobre alinhamento de memória e barreiras de memória
- Go 1.19 Release Notes: atomic package — Notas de lançamento do Go 1.19 detalhando as novas APIs tipadas do pacote atomic
- Performance comparison: atomic vs mutex in Go — Discussão no Stack Overflow com benchmarks reais e análise de desempenho em diferentes cenários de contenção