Programação concorrente básica
Em Go, podemos fazer programação concorrente por meio das goroutines, que permitem executar funções de forma concorrente. Para isso, usamos a palavra-chave go antes da chamada de uma função.
Executando funções de forma concorrente
Imagine um programa que precisa executar duas operações independentes, como consultar usuários e produtos em um banco de dados.
Normalmente, chamaríamos as funções em sequência.
buscarUsuarios()
buscarProdutos()
Nesse caso, buscarProdutos() só começa depois que buscarUsuarios() termina. Para executar essas funções de forma concorrente, podemos adicionar a palavra-chave go.
go buscarUsuarios()
go buscarProdutos()
O go inicia a execução de cada função em uma goroutine, permitindo que elas avancem de forma independente da função que as iniciou, porém, existe um detalhe importante. Quando iniciamos uma goroutine, o programa não espera automaticamente que ela termine. Se a função main() encerrar antes das goroutines, o programa será finalizado mesmo que ainda existam tarefas em execução.
Por isso, precisamos de uma forma de aguardar a conclusão dessas tarefas.
Sincronizando a execução com sync.WaitGroup
O Go possui o package sync, que oferece recursos para sincronizar a execução de diferentes goroutines. Um desses recursos é o WaitGroup, que permite acompanhar um grupo de tarefas e aguardar até que todas tenham terminado.
Podemos usar três operações principais nesse processo:
Add(): Informa quantas tarefas precisamos aguardar.Done(): Indica que uma tarefa terminou.Wait(): Bloqueia a execução da goroutine que fez a chamada até que todas as tarefas registradas tenham sido concluídas.
Exemplo:
package main
import (
"fmt"
"sync"
"time"
)
func executarTarefa(nome string) {
fmt.Println("Iniciando tarefa:", nome)
// Simula uma operação que demora 2 segundos.
time.Sleep(2 * time.Second)
fmt.Println("Tarefa finalizada:", nome)
}
func main() {
// Cria um WaitGroup para controlar as tarefas.
var wg sync.WaitGroup
// Informa que vamos aguardar duas tarefas.
wg.Add(2)
// Inicia a primeira goroutine.
go func() {
defer wg.Done()
executarTarefa("A")
}()
// Inicia a segunda goroutine.
go func() {
defer wg.Done()
executarTarefa("B")
}()
// Aguarda a conclusão das duas goroutines.
wg.Wait()
fmt.Println("Todas as tarefas foram concluídas!")
}
Resultado possível seria:
Iniciando tarefa: B
Iniciando tarefa: A
Tarefa finalizada: A
Tarefa finalizada: B
Todas as tarefas foram concluídas!
Nesse exemplo, criamos duas goroutines que executam a mesma função, mas recebem argumentos diferentes:
go func() {
defer wg.Done()
executarTarefa("A")
}()
Essa construção cria uma função anônima, que representa uma função sem nome, e inicia sua execução em uma goroutine. Dentro dela, usamos defer wg.Done() para informar ao WaitGroup que a tarefa terminou quando a função anônima encerrar.
Depois de iniciar as duas goroutines, chamamos:
wg.Wait()
Nesse momento, a função main() aguarda até que as duas tarefas sejam concluídas. Como cada tarefa simula uma espera de dois segundos, o programa poderá terminar em aproximadamente dois segundos, em vez dos quatro segundos que levaria executando as funções em sequência.
$ time go run .
Iniciando tarefa: B
Iniciando tarefa: A
Tarefa finalizada: A
Tarefa finalizada: B
Todas as tarefas foram concluídas!
real 0m2.030s
user 0m0.038s
sys 0m0.037s
Outro detalhe importante é que a goroutine A é iniciada primeiro no código, mas isso não significa que ela será a primeira a executar. Quando usamos go, estamos pedindo ao Go para iniciar uma função de forma concorrente, porém, quem decide quando cada goroutine será executada é o scheduler do Go, que organiza a execução das goroutines.
Por isso, mesmo iniciando A antes de B, podemos ter este resultado:
Iniciando tarefa: B
Iniciando tarefa: A
Tarefa finalizada: A
Tarefa finalizada: B
Todas as tarefas foram concluídas!
Nesse exemplo, a goroutine B conseguiu mostrar sua primeira mensagem antes da goroutine A, mas a goroutine A terminou sua tarefa primeiro.
Isso acontece porque a ordem de execução das goroutines não é garantida, e o time.Sleep(2 * time.Second) também não garante que uma goroutine continuará executando antes da outra quando o tempo terminar.
O wg.Wait() apenas garante que a função main() aguarde a conclusão das duas goroutines, sem determinar qual delas terminará primeiro, portanto, a ordem em que criamos as goroutines não determina a ordem em que elas executam ou terminam.