Skip to main content


Introdução a Concorrência​


A concorrência é uma forma de organizar a execução de um programa para que várias tarefas possam acontecer ao mesmo tempo, sem que uma tarefa precise esperar que outra termine completamente para começar.


Normalmente, quando executamos um programa, as instruções são processadas em sequência. Isso significa que uma função precisa terminar sua execução para que a próxima seja chamada.


Com a concorrência, podemos organizar tarefas independentes para que elas avancem sem precisar seguir essa ordem, permitindo que o programa continue realizando outras operações enquanto uma tarefa aguarda algum resultado.


Por exemplo, imagine um programa que precisa consultar usuários e produtos em um banco de dados. Sem concorrência, ele poderia consultar os usuários primeiro e somente depois consultar os produtos. Isso é chamdo de execução sequencial.


Com a concorrência, podemos iniciar as duas consultas sem precisar esperar que a primeira termine para começar a segunda. Isso é chamado de execução concorrente.


Isso é muito útil quando uma tarefa precisa aguardar uma resposta externa, como uma consulta ao banco de dados, uma requisição HTTP ou uma operação de leitura de arquivos. Durante essa espera, o programa pode continuar trabalhando em outras tarefas, em vez de ficar dependendo da conclusão da primeira operação.



Diferença entre concorrência e paralelismo​


Embora sejam conceitos relacionados, concorrência e paralelismo não significam a mesma coisa. Na concorrência, várias tarefas podem progredir durante o mesmo período, mesmo que o processador execute apenas uma delas por vez. Nesse caso, o sistema alterna entre as tarefas, permitindo que todas avancem.

Concorrência em um único núcleo

Tempo ──────────────────────────────────>

CPU [ A ][ B ][ A ][ B ][ A ][ B ]

Já no paralelismo, duas ou mais tarefas são executadas ao mesmo tempo, normalmente em diferentes núcleos do processador:

Paralelismo em dois núcleos

Tempo ──────────────────────────────────>

CPU 1 [ A ][ A ][ A ][ A ][ A ]
CPU 2 [ B ][ B ][ B ][ B ][ B ]

A concorrência está relacionada à organização e ao progresso de várias tarefas, enquanto o paralelismo está relacionado à execução simultânea dessas tarefas.


Um programa concorrente também pode executar tarefas em paralelo, dependendo dos recursos disponíveis e de como sua execução é organizada.



Concorrência em Go​


O Go foi desenvolvido com recursos de concorrência integrados à linguagem, facilitando a criação de programas que precisam executar várias tarefas independentes. Isso é útil em servidores, APIs e aplicações de rede, onde várias operações podem acontecer durante o mesmo período.


Por exemplo, um servidor HTTP pode receber várias requisições de diferentes usuários. Se precisasse terminar completamente uma requisição antes de começar outra, o atendimento poderia ficar lento, principalmente quando alguma operação dependesse de uma resposta externa.


Para trabalhar com concorrência, Go oferece recursos como goroutines e channels. As goroutines permitem executar funções de forma concorrente, enquanto os channels permitem trocar informações e sincronizar a execução entre goroutines.


Esses recursos fazem parte da linguagem e ajudam a criar programas concorrentes sem precisar gerenciar diretamente threads do sistema operacional na maioria das situações.


Uma thread é uma unidade de execução que o sistema operacional pode executar e agendar no processador. O Go possui um sistema próprio de gerenciamento das goroutines, que distribui sua execução entre threads do sistema operacional.



Exemplo de execução sequencial​


Antes de trabalhar diretamente com concorrência, vamos observar um programa que executa duas tarefas em sequência.

package main

import (
"fmt"
"time"
)

func buscarUsuarios() {
fmt.Println("Buscando usuários...")

// Simula uma operação que demora 2 segundos.
time.Sleep(2 * time.Second)

fmt.Println("Usuários encontrados!")
}

func buscarProdutos() {
fmt.Println("Buscando produtos...")

// Simula uma operação que demora 2 segundos.
time.Sleep(2 * time.Second)

fmt.Println("Produtos encontrados!")
}

func main() {
buscarUsuarios()
buscarProdutos()
}

O resultado é:

Buscando usuários...
Usuários encontrados!
Buscando produtos...
Produtos encontrados!

Nesse exemplo, usamos time.Sleep() para simular uma operação que demora para terminar, como uma consulta ao banco de dados. Quando chamamos:

buscarUsuarios()
buscarProdutos()

O Go executa primeiro buscarUsuarios() e aguarda sua conclusão. Somente depois começa a executar buscarProdutos(). Como cada função demora aproximadamente dois segundos, o programa levará cerca de quatro segundos para terminar.


Com concorrência, poderíamos organizar essas funções para que fossem executadas de forma independente, permitindo que as duas operações avançassem durante o mesmo período. Nesse caso, como estamos simulando operações de espera, o tempo total poderia ficar próximo de dois segundos, em vez de quatro.


Isso não significa que a concorrência sempre deixa um programa mais rápido. O resultado depende das tarefas executadas, dos recursos disponíveis e do tempo gasto para coordenar a execução.



O que são goroutines​


As goroutines são unidades de execução gerenciadas pelo próprio Go, que permitem executar funções de forma concorrente, sem precisar esperar que uma função termine para iniciar outra.


Diferentemente das threads tradicionais, que são gerenciadas pelo sistema operacional, as goroutines são gerenciadas pelo runtime do Go, que é responsável por organizar sua execução e distribuí-las entre as threads disponíveis.


As goroutines são mais leves que as threads do sistema operacional, porque começam com uma pequena quantidade de memória para sua pilha, que pode crescer conforme a necessidade. Isso permite criar milhares de goroutines em um mesmo programa, dependendo dos recursos disponíveis.



Criando uma goroutine​


Para criar uma goroutine, usamos a palavra-chave go antes da chamada de uma função. Por exemplo, normalmente chamaríamos uma função assim:

executarTarefa()

Nesse caso, o programa aguarda a função terminar antes de continuar. Para executar a função em uma goroutine, adicionamos go:

go executarTarefa()

Agora, a função será executada de forma concorrente, permitindo que o programa continue executando outras instruções sem precisar aguardar sua conclusão.



Como o Go gerencia as goroutines?​


Quando criamos uma goroutine, o Go não cria necessariamente uma nova thread do sistema operacional. Em vez disso, o runtime possui um scheduler, que é responsável por distribuir a execução das goroutines entre as threads disponíveis.


Isso permite que várias goroutines compartilhem um número menor de threads, reduzindo o custo de criar e gerenciar tarefas concorrentes. Podemos representar esse funcionamento assim:

             Programa Go
|
v
+---------------+
| Scheduler |
+---------------+
|
+-------+-------+
| | |
v v v
Thread 1 Thread 2 Thread 3
| | |
v v v
CPU CPU CPU

O scheduler organiza quais goroutines serão executadas nas threads disponíveis, podendo alternar entre elas conforme a necessidade. Por exemplo, quando uma goroutine precisa aguardar uma operação de rede, o Go pode permitir que outras goroutines continuem executando durante essa espera.


Isso não significa que cada goroutine terá uma thread ou um núcleo exclusivo do processador, várias goroutines podem compartilhar as mesmas threads, e sua execução pode acontecer de forma concorrente ou paralela.



Goroutines com funções anônimas​


Também podemos criar goroutines usando funções anônimas, que são funções declaradas sem um nome. Por exemplo:

go func() {
fmt.Println("Executando uma goroutine")
}()

Nesse caso, a função é declarada e iniciada diretamente como uma goroutine. Observe os parênteses no final:

}()

Eles indicam que a função anônima será chamada. A palavra-chave go faz com que essa chamada aconteça em uma nova goroutine. Esse formato é comum quando precisamos executar uma tarefa pequena sem criar uma função separada.



Cuidados ao trabalhar com goroutines​


Embora sejam leves, as goroutines também consomem memória e recursos do programa. Por isso, criar uma quantidade muito grande de goroutines sem necessidade pode causar problemas de desempenho.


Também precisamos lembrar que as goroutines podem executar em ordens diferentes, então não devemos depender da ordem em que foram iniciadas.


Outro cuidado acontece quando várias goroutines acessam e modificam a mesma variável. Dependendo de como isso é feito, podemos ter uma race condition, que acontece quando o resultado de uma operação depende da ordem em que diferentes goroutines acessam os mesmos dados.