No ecossistema Go, o mecanismo fundamental para concorrência é a Goroutine. Diferente de threads tradicionais do sistema operacional, Goroutines são multiplexadas de forma eficiente pelo runtime Go, permitindo a execução simultânea de milhares de operações com sobrecarga mínima.
Conceito Básico e Criação
Uma Goroutine é uma função executada de forma concorrente, iniciada pela palavra-chave go. O runtime do Go gerencia sua alocação e escalonamento automaticamente, sem a necessidade de intervenção direta do programador.
package main
import (
"fmt"
"time"
)
func processar(id int) {
for iter := 0; iter < 3; iter++ {
fmt.Printf("Processo %d: iteração %d\n", id, iter)
time.Sleep(30 * time.Millisecond)
}
}
func main() {
for contador := 0; contador < 3; contador++ {
go processar(contador)
}
time.Sleep(400 * time.Millisecond)
}
Arquitetura do Escalonador M:N
O escalonador do Go utiliza um modelo M:N, onde múltiplas Goroutines (M) são distribuídas entre um número limitado de threads do sistema operacional (N). Essa distribuição é mediada por processadores virtuais (P) que mantêm filas locais de exeecução.
Os componentes principais incluem:
- G (Goroutine): Representa o contexto de execução, incluindo pilha e estado.
- M (Machine): Thread do sistema operacional vinculada a um processador.
- P (Processor): Recurso de execução com fila local de Goroutines.
Gerenciamento de Pilha Adaptativo
Enquanto threads do sistema operacional reservam blocos fixos de memória (geralmente 1MB), as Goroutines começam com aproximadamente 2KB. A pilha expande e contrai dinamicamente conforme necessário, utilizando uma técnica de pilha contígua desde a versão 1.3 do Go.
func calcularRecursivamente(depth int) int {
if depth <= 0 {
return 1
}
return depth + calcularRecursivamente(depth-1)
}
func main() {
go calcularRecursivamente(500000)
time.Sleep(time.Second)
}
Eficiência na Troca de Contexto
A transição entre Goroutines ocorre no espaço do usuário, evitando a sobrecarga de mudanças para o modo kernel. Experimentos demonstram que a criação e alternância entre dezenas de milhares de Goroutines consome apenas frações de segundo.
package main
import (
"runtime"
"sync"
"time"
)
func tarefaLeve() {
soma := 0
for i := 0; i < 100000; i++ {
soma += i
}
}
func main() {
var wg sync.WaitGroup
inicio := time.Now()
for i := 0; i < 100000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
tarefaLeve()
}()
}
wg.Wait()
println("Tempo decorrido:", time.Since(inicio).String())
}
Preempção Assíncrona
Versões anteriores do Go utilizavam escalonamento cooperativo, onde as tarefas precisavam ceder o controle voluntariamente. A partir da versão 1.14, a preempção assíncrona permite que o runtime interrompa Goroutines que ocupam a CPU por muito tempo, melhorando a responsividade do sistema.
Configuração do Paralelismo
A variável GOMAXPROCS define quantos processadores virtuais estão disponíveis para execução paralela. Ajustar este valor conforme a capacidade do hardware pode otimizar significativamente o desempenho em tarefas computacionalmente intensas.
package main
import (
"fmt"
"runtime"
"sync"
"time"
)
func main() {
runtime.GOMAXPROCS(runtime.NumCPU())
var mu sync.Mutex
contador := 0
var wg sync.WaitGroup
for i := 0; i < 1000; i++ {
wg.Add(1)
go func() {
defer wg.Done()
mu.Lock()
contador++
mu.Unlock()
}()
}
wg.Wait()
fmt.Println("Total:", contador)
fmt.Println("CPUs disponíveis:", runtime.NumCPU())
}
O design leve das Goroutines permite a criação de centenas de milhares de unidades de concorrência em aplicações de rede, processamento paralelo e sistemas de alta demanda, superando as limitações das abordagens baseadas em threads tradicionais.