Pular pro conteúdo
Francisco Cidade
6 minutos de leitura

Concorrência e Paralelismo com Golang - Wait Groups

Sumário

Wait Groups 🚀

alt text

O que é sync.WaitGroup? 🤔

WaitGroup é uma estrutura do pacote “sync” que permite sincronizar a execução de múltiplas goroutines. Ele funciona contando quantas goroutines ainda não terminaram sua execução, bloqueando o main até que o contador seja zero.

Como usar WaitGroup?

Exemplo:

package main

import (
	"fmt"
	"sync"
)

func main() {
	wg := &sync.WaitGroup{}
	wg.Add(1)  // Informa que teremos uma goroutine a ser sincronizada

	go func() {
		defer wg.Done() // Garante que o wg.Done() será chamado quando a goroutine terminar
		fmt.Println("Olá, WaitGroup!")
	}()

	// Aguarda todas as goroutines terminarem
	wg.Wait()
}

Note que sync.WaitGroup não possui um construtor. Isso ocorre porque a estrutura WaitGroup é inicializada com seus valores zero, que já são válidos para uso. Portanto, você pode simplesmente declarar uma variável do tipo sync.WaitGroup e começar a usá-la imediatamente.

Usando WaitGroup com múltiplas goroutines em um loop

Exemplo de 5 goroutines:

package main

import (
	"fmt"
	"sync"
)

func main() {
	wg := &sync.WaitGroup{}
	
	// Adiciona 5 antes de iniciar o loop
	for i := 1; i <= 5; i++ {
		wg.Add(1)
		go func(n int) {
			defer wg.Done()
			fmt.Printf("Goroutine %d executando\n", n)
		}(i)
	}
	
	// Aguarda que todas as goroutines terminem
	wg.Wait()
}

Uso com funções que simulam tarefas demoradas

Você pode criar funções que realizam tarefas (como um time.Sleep) e sincronizá-las com o WaitGroup.

Exemplo:

package main

import (
	"fmt"
	"sync"
	"time"
)

// Função que simula um trabalho demorado
func trabalhoDemorado() {
	fmt.Println("Iniciando trabalho demorado")
	time.Sleep(2 * time.Second)
	fmt.Println("Trabalho demorado finalizado")
}

func main() {
	wg := &sync.WaitGroup{}
	wg.Add(1)
	
	go func() {
		defer wg.Done()
		trabalhoDemorado()
	}()
	
	// O main aguarda o término do trabalhoDemorado
	wg.Wait()
}

Cuidados e pontos importantes

Exercícios 📝

  1. Lance uma única goroutine que imprima “Olá, WaitGroup!”.

    • Utilize sync.WaitGroup para controlar a execução.
    • Certifique-se de chamar wg.Done() ao final da goroutine e wg.Wait() no main.
  2. Lance 5 goroutines utilizando um loop for.

    • Cada goroutine deve imprimir uma mensagem identificando seu número (por exemplo, “Goroutine 1”, “Goroutine 2”, etc.).
  3. Crie um programa que contenha uma função chamada trabalhoDemorado, que:

    • Imprima “Iniciando trabalho demorado”,
    • Realize um time.Sleep para simular uma operação que leva 2 segundos,
    • Em seguida, imprima “Trabalho demorado finalizado”.
    • Lance essa função em uma goroutine controlada por um WaitGroup e garanta que o main aguarde a conclusão antes de encerrar.
  4. Crie um programa que lance duas goroutines paralelas:

    • A primeira goroutine deverá, em um loop de 1 a 5, imprimir “Número: X” (onde X é o número atual) e realizar um time.Sleep curto (por exemplo, 200ms) entre as iterações.
    • A segunda goroutine deverá, também em um loop de 1 a 5, imprimir “Letra: A, B, C, …” (você pode definir uma sequência fixa ou utilizar um array de letras) com um time.Sleep semelhante entre as iterações.

Erros Comuns com WaitGroup ⚠️

alt text

Esquecer de chamar wg.Add() antes de iniciar uma goroutine

Não chamar wg.Done() ao final da execução de uma goroutine

Passagem incorreta da referência do WaitGroup

Chamar wg.Add() dentro da goroutine

Solução dos exercícios

Pra não ter que ficar rodando manualmente cada exercício, eu usei testes e exemplos testáveis.

Exercício 1:

// main.go
func Ex1() {
	// Cria uma instância de WaitGroup.
	wg := &sync.WaitGroup{}

	// Adiciona um contador ao WaitGroup.
	wg.Add(1)

	// Inicia uma nova goroutine.
	go func() {
		// Garante que Done será chamado quando a goroutine terminar.
		defer wg.Done()
		// Imprime uma mensagem.
		fmt.Println("Olá, WaitGroup!")
	}()

	// Aguarda até que o contador do WaitGroup seja zero.
	wg.Wait()
}


// main_test.go
func ExampleEx1() {
	Ex1()
	// output: Olá, WaitGroup!
}

Exercício 2:

// main.go
func Ex2() {
	wg := &sync.WaitGroup{}

	for i := range 5 {
		// Atenção! Não esqueça de chamar o wg.Add(1) p/ cada iteração do for
		// OU use um wg.Add(5) antes do for!
		wg.Add(1)
		go func(i int) {
			defer wg.Done()
			fmt.Println("Goroutine", i)
		}(i)

		wg.Wait()
	}
}


// main_test.go
func TestEx2(t *testing.T) {
	out := captureOutput(Ex2)
	expectedOutputs := []string{
		"Goroutine 0",
		"Goroutine 1",
		"Goroutine 2",
		"Goroutine 3",
		"Goroutine 4",
	}
	for _, expected := range expectedOutputs {
		if !strings.Contains(out, expected) {
			t.Fatal("Output not generated: ", expected, " got: ", out)
		}
	}
}

func captureOutput(f func()) string {
	orig := os.Stdout
	r, w, _ := os.Pipe()
	os.Stdout = w
	f()
	os.Stdout = orig
	w.Close()
	out, _ := io.ReadAll(r)
	return string(out)
}

Exercício 3:

// main.go

func trabalhoDemorado(wg *sync.WaitGroup) {
	defer wg.Done()
	fmt.Println("Iniciando trabalho demorado")
	time.Sleep(2 * time.Second)
	fmt.Println("Trabalho demorado finalizado")
}

func Ex3() {
	wg := &sync.WaitGroup{}

	wg.Add(1)
	go func () {
		defer wg.Done()
		trabalhoDemorado(&wg)
	}()

	wg.Wait()
}


// main_test.go
func ExampleEx3() {
	Ex3()
	// output:
	// Iniciando trabalho demorado
	// Trabalho demorado finalizado
}

Exercício 4:

// main.go
func Ex4() {
	wg := &sync.WaitGroup{}

	alphabet := "ABCDEFGHIJKLMNOPQRSTUVWXYZ"

	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := range 5 {
			fmt.Printf("Número: %d\n", i)
			time.Sleep(200 * time.Millisecond)
		}
	}()

	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := range 5 {
			fmt.Printf("Letra: %s\n", string(alphabet[i]))
			time.Sleep(200 * time.Millisecond)
		}
	}()

	wg.Wait()
}

// main_test.go
func TestEx4(t *testing.T) {
	out := captureOutput(Ex4)
	expectedOutputs := []string{
		"Número: 0",
		"Número: 1",
		"Número: 2",
		"Número: 3",
		"Número: 4",
		"Letra: A",
		"Letra: B",
		"Letra: C",
		"Letra: D",
		"Letra: E",
	}
	for _, expected := range expectedOutputs {
		if !strings.Contains(out, expected) {
			t.Fatal("Output not generated: ", expected, " got: ", out)
		}
	}
}

Próximos passos!

Em seguida iremos abordar Channels! Clique aqui para a parte 3!

Referências