GoSuda

Fundamentos de Goroutines

By hamori
views ...

Goroutine

Quando se pede aos Gophers para falar sobre as vantagens do golang, frequentemente surge um artigo relacionado à concorrência, que é um de seus pontos fortes. A base desse conteúdo é a goroutine, que é leve e fácil de manipular. Escrevi brevemente sobre isso.

Concorrência vs. Paralelismo

Antes de entender as goroutines, gostaria de abordar dois conceitos frequentemente confundidos.

  • Concorrência: A concorrência refere-se à capacidade de lidar com muitas tarefas de uma só vez. Não significa necessariamente que elas são executadas simultaneamente, mas sim um conceito estrutural e lógico onde múltiplas tarefas são divididas em unidades menores e executadas alternadamente, fazendo com que o usuário perceba múltiplas tarefas sendo processadas simultaneamente. A concorrência é possível mesmo em um único núcleo.
  • Paralelismo: O paralelismo é a "execução simultânea de múltiplas tarefas em múltiplos núcleos". Literalmente, envolve a execução de tarefas em paralelo, processando diferentes trabalhos ao mesmo tempo.

Goroutines permitem que a concorrência seja facilmente implementada através do escalonador de tempo de execução Go e utilizam o paralelismo naturalmente por meio da configuração GOMAXPROCS.

O multi-threading do Java, frequentemente de alta utilização, é um conceito representativo de paralelismo.

Por que Goroutines são vantajosas?

Leve (lightweight)

O custo de criação é muito baixo em comparação com outras linguagens. Surge a pergunta: por que golang usa menos recursos? A razão é que o local de criação é gerenciado internamente pelo tempo de execução Go. Isso ocorre porque se trata de um lightweight logical thread, menor que uma unidade de thread do OS, requerendo um stack inicial de aproximadamente 2KB e sendo dinamicamente variável, com o stack sendo adicionado conforme a implementação do usuário.

Gerenciadas em unidades de stack, a criação e remoção são muito rápidas e de baixo custo, permitindo o processamento de milhões de goroutines sem sobrecarga. Devido a isso, as Goroutines, graças ao escalonador de tempo de execução, podem minimizar a intervenção do kernel do OS.

Bom desempenho (performance)

Primeiramente, conforme explicado acima, as Goroutines têm pouca intervenção do kernel do OS, o que torna o custo da troca de contexto no nível do usuário (User-Level) mais barato do que as unidades de thread do OS, permitindo uma rápida alternância de tarefas.

Além disso, o modelo M:N é utilizado para alocar e gerenciar threads do OS. Ao criar um pool de threads do OS, é possível processar com um número menor de threads, sem a necessidade de muitas. Por exemplo, se uma Goroutine entra em estado de espera, como uma chamada de sistema, o tempo de execução Go executa outra Goroutine na thread do OS, permitindo que a thread do OS não fique ociosa e utilize eficientemente a CPU para um processamento rápido.

Isso permite que o Golang obtenha um desempenho superior em operações de I/O em comparação com outras linguagens.

Conciso (concise)

A capacidade de lidar facilmente com funções usando a palavra-chave go quando a concorrência é necessária é uma grande vantagem.

É necessário usar Locks complexos como Mutex e Semaphore, e o uso de Locks inevitavelmente requer a consideração do estado de Deadlock, o que exige etapas complexas desde a fase de design antes do desenvolvimento.

Goroutine adere à filosofia "não se comunique compartilhando memória; comunique-se para compartilhar memória", recomendando a passagem de dados através de Channel e SELECT suporta a funcionalidade de processar canais que têm dados prontos em combinação com Channel. Além disso, o uso de sync.WaitGroup permite aguardar facilmente até que todas as goroutines sejam concluídas, facilitando o gerenciamento do fluxo de trabalho. Graças a essas ferramentas, é possível evitar problemas de concorrência de dados entre threads e processar a concorrência de forma mais segura.

Além disso, ao utilizar o contexto (context), é possível controlar o ciclo de vida, cancelamento, timeout, deadline e escopo de solicitação no nível do usuário (User-Level), garantindo um certo grau de estabilidade.

Tarefas paralelas de Goroutine (GOMAXPROCS)

Embora eu tenha mencionado as vantagens da concorrência das goroutines, você pode se perguntar se o paralelismo não é suportado. O número de núcleos de CPU hoje é de dezenas, diferente do passado, e mesmo PCs domésticos têm um número considerável de núcleos.

No entanto, as Goroutines também realizam trabalho paralelo, e isso é GOMAXPROCS.

Se GOMAXPROCS não for definido, ele será configurado de forma diferente dependendo da versão.

  1. Antes da 1.5: Valor padrão 1, e era obrigatório definir para 1 ou mais usando runtime.GOMAXPOCS(runtime.NumCPU()) ou similar.

  2. 1.5 a 1.24: Foi alterado para usar todos os núcleos lógicos disponíveis. A partir daí, os desenvolvedores não precisaram mais configurá-lo, a menos que houvesse uma restrição específica.

  3. 1.25: Como uma linguagem popular em ambientes de contêiner, ele verifica o cGroup no Linux para determinar o limite de CPU configurado para o contêiner.

    Assim, se o número de núcleos lógicos for 10 e o limite de CPU for 5, GOMAXPROCS será definido para o número menor, 5.

A modificação da versão 1.25 representa uma mudança significativa. Isso porque a utilidade da linguagem em ambientes de contêiner aumentou. Consequentemente, foi possível reduzir a criação desnecessária de threads e as trocas de contexto, prevenindo o estrangulamento (throttling) da CPU.

 1package main
 2
 3import (
 4	"fmt"
 5	"math/rand"
 6	"runtime"
 7	"time"
 8)
 9
10func exe(name int, wg *sync.WaitGroup) {
11	defer wg.Done()
12
13	fmt.Printf("Goroutine %d: 시작\n", name) // Goroutine %d: Início
14	time.Sleep(10 * time.Millisecond) // Atraso para simular o trabalho
15	fmt.Printf("Goroutine %d: 시작\n", name) // Goroutine %d: Início
16}
17
18func main() {
19	runtime.GOMAXPROCS(2) // Usar apenas 2 núcleos de CPU
20	wg := sync.WaitGroup();
21  goroutineCount := 10
22	wg.Add(goroutineCount)
23
24	for i := 0; i < goroutineCount; i++ {
25		go exe(i, &wg)
26	}
27
28	fmt.Println("모든 goroutine이 끝날 때까지 대기합니다...") // Aguardando todas as goroutines terminarem...
29	wg.Wait()
30	fmt.Println("모든 작업이 완료되었습니다.") // Todas as tarefas foram concluídas.
31
32}
33

Escalador de Goroutine (Modelo M:N)

Aprofundando-nos um pouco mais na seção anterior sobre o modelo M:N para alocar e gerenciar threads do OS, temos o modelo GMP da goroutine.

  • G (Goroutine): A menor unidade de trabalho executada em Go.
  • M (Machine): Thread do OS (local de execução real).
  • P (Processor): Processo lógico gerenciado pelo tempo de execução Go.

P adicionalmente possui uma fila de execução local (Local Run Queue) e atua como um escalonador que atribui Gs alocadas a Ms. Em termos simples, a goroutine é

O processo de operação do GMP é o seguinte:

  1. Quando uma G (Goroutine) é criada, ela é alocada na fila de execução local de um P (Processor).
  2. O P (Processor) aloca a G (Goroutine) da sua fila de execução local a um M (Machine).
  3. O M (Machine) retorna o estado da G (Goroutine): block, complete, ou preempted.
  4. Work-Stealing (Roubo de Trabalho): Se a fila de execução local de um P ficar vazia, outro P verifica a fila global. Se também não houver Gs lá, ele "rouba" trabalho de outro P (Processor) local para garantir que todos os Ms estejam sempre ativos.
  5. Tratamento de Chamada de Sistema (Blocking): Se uma G (Goroutine) em execução for bloqueada, o M (Machine) entra em estado de espera. Nesse momento, o P (Processor) se desvincula do M (Machine) bloqueado e se combina com outro M (Machine) para executar a próxima G (Goroutine). Isso evita o desperdício de CPU mesmo durante o tempo de espera de operações de I/O.
  6. Se uma G (Goroutine) monopolizar o processador (preempted) por muito tempo, ela cede a oportunidade de execução a outra G (Goroutine).

O coletor de lixo (GC) do Golang também é executado em Goroutines, o que permite a limpeza paralela da memória com interrupções mínimas da execução da aplicação (STW), utilizando os recursos do sistema de forma eficiente.

Finalmente, o Golang possui uma das fortes vantagens da linguagem, e há muitas outras, então espero que muitos desenvolvedores aproveitem o Go.

Obrigado.