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. ;)

Página principal do blog

Meus links de afiliados:

Hostinger

Digital Ocean

One.com

Obrigado e bons estudos. ;)