Aula 37 - Tutorial Golang - WaitGroups
Introdução
As
WaitGroups são uma ferramenta poderosa em
Go para sincronizar
goroutines e controlar o fluxo de execução em programas concorrentes.
Elas são úteis quando você precisa garantir que todas as
goroutines concluam suas tarefas antes que o programa principal termine ou antes de prosseguir para a próxima etapa.
Nesta aula, vamos explorar
WaitGroups e aprender a usá-las com exemplos práticos e casos de uso.
O que é uma WaitGroup?
Uma
WaitGroup é uma estrutura de dados fornecida pela biblioteca padrão de
Go que permite esperar que um conjunto de goroutines seja concluído antes de continuar a execução do programa principal.
Ela faz isso mantendo uma contagem interna de
goroutines e
bloqueando o
programa principal até que todas as
goroutines tenham sinalizado que terminaram.
Exemplo 1: WaitGroup Básica
Vamos começar com um exemplo simples para entender como uma
WaitGroup funciona.
package main
import (
"fmt"
"sync"
)
func main() {
var wg sync.WaitGroup
// Criando três goroutines simulando tarefas independentes
for i := 0; i < 3; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
fmt.Printf("Goroutine %d executando\n", id)
}(i)
}
// Aguardando todas as goroutines concluírem
wg.Wait()
fmt.Println("Todas as goroutines concluídas")
}
Importações:
O programa importa as bibliotecas "
fmt" e "
sync" para impressão de saída e manipulação de sincronização de concorrência, respectivamente.
Criação da WaitGroup:
É criada uma
WaitGroup chamada "
wg" usando a declaração
var wg sync.WaitGroup.
Loop para Criar Goroutines:
Um loop for é usado para criar três goroutines simulando tarefas independentes.
wg.Add(1) é chamado dentro do loop para
adicionar 1 ao
contador da
WaitGroup para cada
goroutine que será criada.
Isso aumenta o contador em
1 para cada goroutine, indicando que
três goroutines precisam ser esperadas.
Em seguida, é iniciada uma nova
goroutine anônima com
go func(id int) { ... }(i).
Cada
goroutine recebe um argumento "
id" que é igual ao valor atual de "
i" no loop.
Goroutines:
Cada
goroutine imprime uma mensagem indicando que está executando e usa defer
wg.Done() para adiar a chamada a
wg.Done().
Isso significa que
wg.Done() será chamado quando a goroutine for concluída, o que
decrementará o contador da
WaitGroup em
1.
Aguardando Todas as Goroutines:
Após a criação das três
goroutines, o programa principal chama
wg.Wait().
Isso faz com que o programa principal aguarde até que o contador da
WaitGroup seja zero.
O programa fica bloqueado até que todas as três goroutines tenham chamado
wg.Done().
Mensagem de Conclusão:
Após todas as
goroutines serem concluídas, ou seja, o contador da
WaitGroup ser zero, o programa imprime "
Todas as goroutines concluídas" usando
fmt.Println.
Exemplo 2: Uso em um Pool de Trabalhadores
Um caso de uso comum para
WaitGroups é em um pool de trabalhadores, onde várias
goroutines trabalham em tarefas diferentes e o programa principal aguarda a conclusão de todas as tarefas.
package main
import (
"fmt"
"sync"
"time"
)
func worker(id int, wg *sync.WaitGroup) {
defer wg.Done()
fmt.Printf("Worker %d iniciado\n", id)
time.Sleep(time.Second)
fmt.Printf("Worker %d concluído\n", id)
}
func main() {
var wg sync.WaitGroup
numWorkers := 5
for i := 0; i < numWorkers; i++ {
wg.Add(1)
go worker(i, &wg)
}
wg.Wait()
fmt.Println("Todas as tarefas concluídas")
}
Importações:
O programa importa as bibliotecas "fmt", "sync" e "time". "fmt" é usada para impressão de saída, "sync" para sincronização de concorrência e "time" para lidar com o tempo.
Função do Trabalhador (worker):
A função
worker representa o trabalho que cada
goroutine realizará.
defer wg.Done() é usado para garantir que
wg.Done() seja chamado quando a
goroutine for concluída.
Isso
decrementa o
contador da
WaitGroup em
1.
fmt.Printf("Worker %d iniciado\n", id) imprime uma mensagem indicando que o trabalhador foi iniciado.
time.Sleep(time.Second) é usado para
simular algum trabalho demorado (
1 segundo de espera).
fmt.Printf("Worker %d concluído\n", id) imprime uma mensagem indicando que o trabalhador foi concluído.
Função Principal (main):
Uma
WaitGroup chamada "
wg" é criada para coordenar a sincronização das goroutines.
A variável "
numWorkers" é definida para o número de
goroutines que serão criadas (5 no caso).
Loop para Criar Goroutines:
Um
loop for é usado para criar
cinco goroutines, simulando cinco tarefas independentes.
wg.Add(1) é chamado dentro do loop para adicionar 1 ao contador da
WaitGroup para cada
goroutine que será criada.
Isso
aumenta o
contador em
1 para cada
goroutine, indicando que cinco
goroutines precisam ser esperadas.
Em seguida, é iniciada uma nova
goroutine chamando
go worker(i, &wg).
Cada
goroutine recebe um argumento "
i" que é igual ao valor atual do loop.
Aguardando Todas as Goroutines:
Após a criação das cinco goroutines, o programa principal chama
wg.Wait().
Isso faz com que o programa principal aguarde até que o contador da
WaitGroup seja zero.
O programa fica bloqueado até que todas as cinco
goroutines tenham chamado
wg.Done().
Conclusão:
Após todas as
goroutines serem concluídas (ou seja, o contador da
WaitGroup é
zero), o programa imprime "
Todas as tarefas concluídas" usando
fmt.Println.
O
time.Sleep(time.Second) no worker é usado para simular tarefas demoradas, e o
defer wg.Done() garante que o contador da
WaitGroup seja decrementado corretamente, mesmo se ocorrerem erros ou exceções nas
goroutines.
Caso de Uso 3: Aguardando Várias Requisições HTTP
Você pode usar
WaitGroups para aguardar a conclusão de várias requisições
HTTP concorrentes.
Cada
goroutine faz uma solicitação HTTP e, quando todas as solicitações forem concluídas, o programa principal pode processar os resultados.
package main
import (
"fmt"
"net/http"
"sync"
)
func fetchURL(url string, wg *sync.WaitGroup) {
defer wg.Done()
resp, err := http.Get(url)
if err != nil {
fmt.Printf("Erro ao buscar URL %s: %v\n", url, err)
return
}
defer resp.Body.Close()
fmt.Printf("URL %s retornou com status: %s\n", url, resp.Status)
}
func main() {
var wg sync.WaitGroup
urls := []string{"https://example.com", "https://google.com", "https://github.com"}
for _, url := range urls {
wg.Add(1)
go fetchURL(url, &wg)
}
wg.Wait()
fmt.Println("Todas as requisições HTTP concluídas")
}
Importações:
O programa importa as bibliotecas "
fmt" para impressão de saída e "
net/http" para realizar solicitações
HTTP.
Função fetchURL:
A função
fetchURL é responsável por buscar uma
URL usando uma solicitação
HTTP GET.
defer wg.Done() é usado para garantir que
wg.Done() seja chamado quando a
goroutine for concluída, o que decrementa o contador da
WaitGroup em
1.
http.Get(url) faz uma solicitação
HTTP GET à
URL fornecida.
Se houver um erro durante a solicitação, ele é tratado e um erro é impresso na saída.
defer resp.Body.Close() é usado para garantir que o corpo da resposta
HTTP seja fechado após o uso.
Finalmente, o status da resposta
HTTP é impresso.
Função Principal (main):
Uma
WaitGroup chamada "
wg" é criada para coordenar a sincronização das goroutines.
Uma
slice de
URLs é definida em "
urls", contendo
três URLs para buscar.
Loop para Buscar URLs:
Um
loop for é usado para percorrer cada
URL em "
urls".
wg.Add(1) é chamado dentro do
loop para adicionar
1 ao
contador da
WaitGroup para cada
URL, indicando que
três URLs precisam ser buscadas.
Em seguida, é iniciada uma
nova goroutine chamando
go fetchURL(url, &wg).
Cada
goroutine busca uma
URL específica.
Aguardando Todas as Goroutines:
Após a criação das
três goroutines, o programa principal chama
wg.Wait().
Isso faz com que o programa principal aguarde até que o contador da
WaitGroup seja
zero.
O programa fica bloqueado até que todas as
três goroutines tenham chamado
wg.Done().
Conclusão:
Após todas as goroutines serem concluídas, isto é, o contador da
WaitGroup ser zero, o programa imprime "
Todas as requisições HTTP concluídas" usando
fmt.Println.
Isso indica que todas as solicitações
HTTP foram concluídas.
Caso de Uso 4: Processamento de Dados em Lote
Você pode usar
WaitGroups ao processar dados em lote, onde várias
goroutines processam partes diferentes dos dados.
O programa principal pode aguardar até que todas as partes dos dados tenham sido processadas antes de prosseguir.
package main
import (
"fmt"
"sync"
)
func processDataBatch(batch []int, wg *sync.WaitGroup) {
defer wg.Done()
sum := 0
for _, num := range batch {
sum += num
}
fmt.Printf("Soma dos elementos no lote: %d\n", sum)
}
func main() {
var wg sync.WaitGroup
data := []int{1, 2, 3, 4, 5, 6, 7, 8}
batchSize := 2
for i := 0; i < len(data); i += batchSize { wg.Add(1) end := i + batchSize if end > len(data) {
end = len(data)
}
go processDataBatch(data[i:end], &wg)
}
wg.Wait()
fmt.Println("Processamento de dados em lote concluído")
}
Importações:
O código faz uso da biblioteca padrão do Go, sem importações adicionais.
Função processDataBatch:
func processDataBatch(batch []int, wg *sync.WaitGroup): Esta função é responsável por processar um lote (ou batch) de números inteiros.
defer wg.Done(): Usa defer para adiar a chamada de
wg.Done(), o que decrementa o contador da
WaitGroup (wg) quando a
goroutine é concluída.
Calcula a soma dos elementos no lote e imprime o resultado usando
fmt.Printf.
Função main:
func main(): A função principal do programa.
Cria uma variável
wg do tipo
sync.WaitGroup para coordenar a execução das
goroutines.
Define uma
slice chamada
data que contém os dados a serem processados, no caso,
números de
1 a
8.
batchSize define o tamanho do lote a ser processado de uma vez, neste caso,
2.
Loop para Processar em Lotes:
O
loop for é usado para dividir os dados em lotes e processá-los em paralelo.
wg.Add(1) é chamado dentro do loop para adicionar
1 ao contador da
WaitGroup (wg) para cada lote, indicando quantos lotes precisam ser processados.
O índice
end é calculado para determinar onde termina o lote atual.
Se o próximo lote estiver além do tamanho dos dados, ele é ajustado para o tamanho dos dados.
Uma
nova goroutine é iniciada chamando
go processDataBatch(data[i:end], &wg).
Cada
goroutine processa um lote específico dos dados.
Aguardando Todas as Goroutines:
Após a criação das
goroutines para processar os lotes, o programa principal chama
wg.Wait().
Isso faz com que o programa principal aguarde até que o
contador da
WaitGroup (wg) seja
zero.
O programa fica bloqueado até que todas as
goroutines tenham chamado
wg.Done().
Conclusão:
Após todas as
goroutines terem processado os lotes, ou seja, o contador da
WaitGroup ser
zero, o programa imprime "
Processamento de dados em lote concluído" usando
fmt.Println.
Isso indica que o processamento dos dados em lotes foi concluído.
Eu fico por aqui.
Até a próxima. ;)
Meus links de afiliados:
Obrigado e bons estudos. ;)