Aula 40 - Tutorial Golang - Atomic Counters
Go foi feito para facilitar o desenvolvimento de programas que podem executar várias tarefas simultaneamente, de forma eficiente e mais simples de entender e manter do que em outras linguagens.
Isso é chamado de programação concorrente.
Vamos ver agora 3 exemplos comuns de uso do pacote
sync/atomic em Go.
1. Contadores Atômicos em Go: Sincronização Concorrente
Cenário: Consideremos um cenário onde precisamos de um contador global para rastrear o número de operações ou eventos ocorridos, acessível por múltiplas goroutines. A abordagem tradicional poderia envolver o uso de mutexes para evitar o acesso simultâneo, o que pode ser menos eficiente. Utilizando o
sync/atomic, podemos implementar um contador atômico que garante a precisão e a performance, mesmo sob alta concorrência.
O exemplo a seguir demonstra a criação de um contador atômico para rastrear o número de operações realizadas por 50 goroutines, cada uma incrementando o contador 1000 vezes. Utilizamos
sync.WaitGroup para sincronizar a conclusão das goroutines, garantindo que a leitura final do contador aconteça apenas após todas as operações serem concluídas. Este método oferece uma solução eficaz e performática para gerenciamento de estado concorrente em Go.
Código
package main
import (
"fmt"
"sync"
"sync/atomic"
)
func main() {
// Vamos usar um tipo primitivo uint64 para representar nosso
// contador (sempre positivo) e realizar operações atômicas sobre ele.
var ops uint64
// Um WaitGroup nos ajudará a esperar que todas as goroutines
// terminem seu trabalho.
var wg sync.WaitGroup
// Vamos iniciar 50 goroutines que cada uma incrementa o
// contador exatamente 1000 vezes.
for i := 0; i < 50; i++ {
wg.Add(1)
go func() {
for c := 0; c < 1000; c++ {
// Para incrementar atomicamente o contador usamos `AddUint64`.
atomic.AddUint64(&ops, 1)
}
wg.Done()
}()
}
// Esperar até que todas as goroutines estejam concluídas.
wg.Wait()
// Aqui nenhuma goroutine está escrevendo em 'ops', mas usando
// `LoadUint64` é seguro ler atomicamente um valor mesmo enquanto
// outras goroutines estão atualizando-o (atomicamente).
fmt.Println("ops:", atomic.LoadUint64(&ops))
}
Explicação:
- Declaração de Variáveis:
var ops uint64: Declara um contador atômico chamado ops, que será usado para contar eventos (operações) de forma segura entre múltiplas goroutines.
var wg sync.WaitGroup: Utiliza um WaitGroup para sincronizar a conclusão de todas as goroutines antes de seguir.
- Execução de Goroutines:
- Inicia 50 goroutines (
for i := 0; i < 50; i++), cada uma incrementando o contador ops 1000 vezes. Isso é feito de maneira atômica com ops.Add(1), garantindo que não haja condições de corrida no acesso ao contador.
- Sincronização das Goroutines:
wg.Add(1) informa ao WaitGroup que uma goroutine está sendo iniciada, e wg.Done() é chamado dentro de cada goroutine após completar seu loop, indicando que terminou sua execução.
wg.Wait() bloqueia a execução até que todas as goroutines tenham chamado wg.Done(), garantindo que todas as operações de incremento tenham sido concluídas antes de prosseguir.
- Leitura do Contador:
- Após todas as goroutines terminarem, o valor final do contador
ops é lido com ops.Load(). Essa operação também é atômica, assegurando uma leitura segura do valor final do contador, que reflete todas as 50.000 operações de incremento realizadas pelas goroutines.
O código ilustra a eficácia do uso de operações atômicas para evitar condições de corrida e a importância da sincronização entre goroutines para garantir que o estado compartilhado seja atualizado e lido de forma segura.
2. Contador Atômico para Monitoramento de Estado
Cenário: Um serviço web onde múltiplas goroutines manipulam um contador que rastreia o número total de requisições recebidas.
Código
package main
import (
"fmt"
"net/http"
"sync/atomic"
)
var (
requestCount uint64
)
func requestHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path == "/" {
atomic.AddUint64(&requestCount, 1)
}
fmt.Fprintf(w, "Request number: %d", atomic.LoadUint64(&requestCount))
}
func main() {
http.HandleFunc("/", requestHandler)
fmt.Println("Servidor rodando na porta 8080...")
http.ListenAndServe(":8080", nil)
err := http.ListenAndServe(":8080", nil)
if err != nil {
fmt.Printf("Erro ao iniciar o servidor: %v\n", err)
}}
Explicação:
requestCount é um contador atômico usado para rastrear o número de requisições.
- Cada vez que uma requisição é recebida,
atomic.AddUint64(&requestCount, 1) incrementa o contador de forma atômica.
atomic.LoadUint64(&requestCount) é usado para ler o valor atual do contador de forma atômica e segura para exibir ao usuário.
- Isso garante que o contador seja incrementado corretamente, mesmo com múltiplas requisições simultâneas.
3. Sistema Básico de Votação em Tempo Real
Cenário: Imagine um cenário em que usamos contadores atômicos para implementar um sistema básico de votação em tempo real em uma aplicação web. Neste exemplo, cada requisição incrementa um contador atômico correspondente a uma opção de voto. Isso pode ser útil, por exemplo, em um sistema de enquetes ao vivo ou em uma funcionalidade de reação em tempo real, onde precisamos garantir a contagem precisa de votos sob alta concorrência.
Exemplo de Código: Sistema de Votação em Tempo Real com Contadores Atômicos
package main
import (
"fmt"
"net/http"
"strconv"
"sync/atomic"
)
var (
// Contadores atômicos para cada opção de voto
votesOptionA uint64
votesOptionB uint64
)
func voteHandler(w http.ResponseWriter, r *http.Request) {
option := r.URL.Query().Get("option")
switch option {
case "A":
atomic.AddUint64(&votesOptionA, 1)
fmt.Fprintf(w, "Voto para a opção A registrado. Total: %d\n", atomic.LoadUint64(&votesOptionA))
case "B":
atomic.AddUint64(&votesOptionB, 1)
fmt.Fprintf(w, "Voto para a opção B registrado. Total: %d\n", atomic.LoadUint64(&votesOptionB))
default:
http.Error(w, "Opção inválida", http.StatusBadRequest)
}
}
func resultsHandler(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Resultados da Votação:\nOpção A: %d votos\nOpção B: %d votos\n",
atomic.LoadUint64(&votesOptionA),
atomic.LoadUint64(&votesOptionB))
}
func main() {
http.HandleFunc("/vote", voteHandler)
http.HandleFunc("/results", resultsHandler)
fmt.Println("Servidor de votação iniciado na porta 8080")
http.ListenAndServe(":8080", nil)
}
Teste:
curl "http://localhost:8080/vote?option=A"
curl "http://localhost:8080/vote?option=B"
curl "http://localhost:8080/results"
Explicação do Código
- Contadores Atômicos para Opções de Voto: Aqui,
votesOptionA e votesOptionB são contadores atômicos que rastreiam os votos para duas opções diferentes, A e B.
- Handler de Votação: A função
voteHandler é responsável por processar as requisições de voto. Ela verifica qual opção foi votada (A ou B) e incrementa o contador atômico correspondente.
- Handler de Resultados: A função
resultsHandler mostra os resultados atuais da votação, acessando de forma atômica os valores dos contadores de votos.
- Servidor HTTP: O servidor está configurado para responder a requisições de votação e exibição de resultados nas rotas
/vote e /results, respectivamente.
Este exemplo ilustra como contadores atômicos podem ser usados em uma aplicação web para gerenciar um sistema de votação em tempo real.
Em um cenário com muitos usuários votando simultaneamente, os contadores atômicos garantem que cada voto seja contabilizado de forma precisa e segura, evitando condições de corrida e inconsistências nos dados.
Eu fico por aqui.
A gente se vê na próxima. \o/
Meus links de afiliados:
Obrigado e bons estudos. ;)