Concorrência em Go com Goroutines

Descubra o poder das goroutines em Go para concorrência eficiente. Aprenda canais, select e tratamento de erros, além das diferenças com threads. Torne seus programas mais escaláveis e eficazes.

Olá, vamos falar sobre goroutines, uma das características mais poderosas do Go. As goroutines são uma ferramenta fundamental para lidar com  concorrência em Go, ela facilita o design de programas que podem executar várias tarefas simultaneamente.

1. O que são Goroutines?

Goroutines são funções ou métodos que são executados independentemente das outras funções de forma concorrente. Elas são diferentes das tradicionais threads, pois têm menos sobrecarga e complexidade. Em Go, iniciar uma goroutine é muito simples, basta prefixar uma chamada de função com a palavra-chave “go”.

go myFunction()

2. Goroutines vs Threads

Antes de definirmos as diferenças, vamos definir ambos os conceitos. Uma thread é a menor sequência de instruções programadas que pode ser gerenciada de forma independente por um agendador de sistema operacional. Ela é um processo leve e independente que compartilha recursos do processo principal, como espaço de memória e arquivos abertos.

Já uma goroutine é uma abstração ainda mais leve de uma thread. Em Go, uma goroutine é uma função que é capaz de rodar simultaneamente com outras funções. É uma abordagem de Go para lidar com tarefas concorrentes e paralelas, tornando mais fácil criar programas altamente eficientes.

2.1. Diferenças principais

  • Sobrecarga de Memória: A primeira e mais importante diferença é a sobrecarga de memória. As goroutines são muito mais eficientes em termos de memória do que as threads. Uma goroutine tem uma “stack” de apenas alguns kilobytes que pode crescer ou diminuir conforme necessário, enquanto uma thread em um sistema operacional requer uma quantidade fixa de memória, geralmente medindo em megabytes. Isso significa que você pode ter milhões de goroutines rodando simultaneamente em um programa Go.
  • Escalonamento: As goroutines são escalonadas pelo “runtime” do Go, não pelo sistema operacional. Isso significa que o escalonamento é mais eficiente, já que o runtime do Go tem uma compreensão melhor do que as goroutines estão fazendo e pode tomar decisões de escalonamento mais inteligentes. Em contraste, as threads são escalonadas pelo sistema operacional, o que pode ser menos eficiente.
  • Criação e destruição: As goroutines são mais fáceis e rápidas de criar e destruir do que as threads. Isso ocorre porque, como mencionamos anteriormente, as goroutines são gerenciadas pelo runtime do Go, que foi projetado para lidar com um grande número delas.
  • Comunicação: Go fornece um mecanismo embutido para a comunicação segura entre goroutines — os Canais. Eles são usados para passar mensagens entre goroutines de maneira segura e sem a necessidade de bloqueios ou variáveis de condição (falaremos mais sobre canais). Em contraste, a comunicação entre threads em outras linguagens pode ser complicada e propensa a erros.

3. Como funcionam as Goroutines?

Go possui um agendador de goroutines incorporado que trata da orquestração de suas execuções. Esse agendador, também conhecido como Go Scheduler, é uma parte crucial da linguagem que lida com a multiplexação de goroutines em cima das threads do sistema operacional. Ele é responsável por gerenciar e distribuir a execução das goroutines nas threads disponíveis. Vamos entender como funciona o agendador de goroutines:

3.1. Entidades Principais: M (Máquina), P (Processador), G (Goroutine)

O Agendador trabalha com três entidades principais: M, P e G.

  • M representa uma thread do sistema operacional.
  • P representa um contexto de execução, que contém informações como a fila de goroutines a serem executadas.
  • G representa uma goroutine.

3.2. Execução e Escalonamento

O agendador adota um modelo de concorrência cooperativa, o que significa que as goroutines precisam ceder explicaitamente o controle para que outras goroutines possam ser executadas. Isso ocorre principalmente quando uma goroutine está aguardando uma operação de I/O (input/output ou entrada/saída) ou está em uma operação de bloqueio de canal (veremos adiante mais sobre canais).

Cada P possui uma fila local de goroutines (G) e todas as P’s compartilham uma única fila global de goroutines. O agendador do Go tenta primeiro retirar uma goroutine da fila local do P. Se a fila local estiver vazia, ele tentará retirar uma goroutine da fila global ou de outra fila local de P aleatória.

3.3. Escalonamento de Trabalho e Roubo de Trabalho

Se todas as filas locais estiverem vazias, mas ainda houver goroutines na fila global, o agendador distribuirá as goroutines da fila global para as filas locais, um processo conhecido como escalonamento de trabalho.

Além disso, o agendador também suporta um conceito chamado roubo de trabalho. Se a fila local de uma P estiver vazia, essa P pode “roubar” metade das goroutines de outra P cuja fila local está mais cheia.

3.4. Netpoller

A última parte crucial do Agendador é o “netpoller“. É uma funcionalidade que coloca goroutines que estão aguardando operações de I/O em uma lista separada e as acorda quando a operação I/O está pronta. Isso evita que threads sejam desperdiçadas em operações de bloqueio de I/O.

Em resumo, o agendador de goroutines do Go é um sistema complexo que gerencia a execução de goroutines de maneira eficiente e justa, garantindo que cada goroutine tenha a chance de ser executada em uma das M’s disponíveis.

4. Comunicação entre Goroutines

As goroutines comunicam-se entre si por meio de um conceito chamado canais. Canais são a forma segura de trocar dados entre goroutines sem a necessidade de bloqueios ou “race conditions“. Para enviar ou receber dados de um canal, você usaria os operadores “<-” e “->”.

ch := make(chan int)  // criação de um canal

go func() { ch <- doSomething() }()  // envia o retorno de doSomething() para o canal

result := <-ch  // recebe o valor do canal e armazena na variável result

5. Selecionando entre Canais

Golang fornece a estrutura de controle select. Essa estrutura de controle é usada para escolher entre múltiplas operações de comunicação em canais. É uma construção poderosa que permite o gerenciamento de múltiplos canais de forma concorrente, tornando mais fácil lidar com situações onde você precisa lidar com várias operações de comunicação.

Aqui está um exemplo básico de como a instrução select é usada:

select {
case msg1 := <-channel1:
    fmt.Println("received", msg1)
case msg2 := <-channel2:
    fmt.Println("received", msg2)
}

Neste exemplo, o  select está esperando até que um dos canais esteja pronto para realizar uma operação de recebimento. Se ambos “channel1” e “channel2” estiverem prontos, o select escolherá um deles aleatoriamente.

Também é possível usar select para implementar um timeout:

select {
case res := <-resultChannel:
    fmt.Println("received result", res)
case <-time.After(1 * time.Second):
    fmt.Println("timeout")
}

Neste caso, se o “resultChannel” não retornar um resultado dentro de um segundo, o select irá executar o “case” do timeout e imprimir “timeout”.

É importante notar que se nenhum dos casos estiver pronto, o select irá bloquear até que um deles esteja. Para evitar isso, você pode usar a cláusula default, que será executada se nenhum outro caso estiver pronto:

select {
case msg := <-channel:
    fmt.Println("received message", msg)
default:
    fmt.Println("no message received")
}

Select é uma ferramenta valiosa quando você precisa gerenciar várias operações de comunicação entre canais.

6. Tratando Erros

Diferente de muitas linguagens de programação que usam exceções para o tratamento de erros, Go adota uma abordagem diferente. Em Go, os erros são tratados como um tipo de valor, geralmente o segundo valor retornado por uma função. A convenção é que este valor de erro será nil se nada deu errado e conterá informações sobre o erro se algo deu errado.

Goroutines são extremamente simples de iniciar, mas é preciso tomar cuidado para não ignorar erros. Recuperar-se de erros de goroutines (panics) envolve o uso da função “recover()“, que deve ser chamada dentro de uma função deferida (defer function…).

6.1. Goroutines e Panics

Aqui está a pegadinha: se uma goroutine entra em panic (um estado causado por erros que não são recuperáveis), ela não afeta outras goroutines. No entanto, se não for tratado, o panic terminará o programa inteiro. Por isso, quando trabalhamos com goroutines, precisamos estabelecer um bom sistema de recuperação e tratamento de erros para evitar panics não tratados que podem levar à interrupção do programa.

6.2. Recuperando-se de Panics

A função “recover” de Go pode ser usada para retomar o controle do panic. A função recover só pode ser chamada dentro de funções deferidas e permite que você capture um “panic”, recupere a execução normal e continue de onde parou. Aqui está um exemplo:

go func() {
    defer func() {
        if r := recover(); r != nil {
            fmt.Println("Recovered from", r)
        }
    }()
    // código da goroutine que pode causar um panic
}()

Neste código, se a goroutine entrar em “panic”, a função “recover” será chamada, capturará o “panic” e permitirá que o programa continue executando.

6.3. Comunicando Erros

Uma abordagem comum para lidar com erros em goroutines é usar canais. Você pode criar um canal de erros e passá-los de uma goroutine para a goroutine principal sempre que ocorrer um erro. A goroutine principal pode então lidar com o erro de maneira adequada.

errc := make(chan error)

go func() {
    // supondo que a função 'doSomething' retorne um erro
    if err := doSomething(); err != nil {
        errc <- err
    }
}()

// na goroutine principal
err := <-errc // recebe o erro, se houver
if err != nil {
    // lidar com o erro
}

Com o uso inteligente de “recover” e canais de erros, você pode efetivamente gerenciar erros e evitar “panics” não tratados em suas goroutines.

Conclusão

As goroutines são um recurso muito poderoso em Golang que facilita a programação concorrente. A capacidade de executar tarefas de maneira eficiente e simultânea, com uma sintaxe simplificada, é parte do que torna Go uma escolha perfeita para o desenvolvimento de sistemas altamente escaláveis. Essa é talvez, a feature mais notável do Go.
Portanto, da próxima vez que você encontrar um problema que exige multitarefa, pense em goroutines e como elas podem tornar sua vida muito mais fácil!

Até a próxima e happy coding!

Deixe um comentário

O seu endereço de e-mail não será publicado. Campos obrigatórios são marcados com *

Post Anterior
optical image recogonition

Extraindo texto de imagens com Node

Próximo Post

Criando um bot com IA para seu WhatsApp com Node.js

Posts Relacionados