DEV Community

Cover image for Workloads: CPU-Bound e IO-Bound
jefferson otoni lima
jefferson otoni lima

Posted on • Updated on

Workloads: CPU-Bound e IO-Bound

E o que é Workloads ? E por que estamos falando disto ?

Será que é para melhor entender quando utilizar concorrência em Go ❤️?

A resposta a esta pergunta é sim 😁, e não somente para Go como para qualquer tecnologia que venha trabalhar com paralelismo e concorrência 😁.

Workload se refere à quantidade de trabalho que uma aplicação precisa realizar. Em outras palavras, o workload é o conjunto de tarefas ou operações que uma aplicação precisa realizar para atender aos seus requisitos 👊🏼.

O workload pode ser distribuído entre vários processos ou threads para melhorar o desempenho da aplicação. Por exemplo, se uma aplicação tem um workload intensivo em CPU, ele pode ser dividido em várias partes menores que podem ser executadas em paralelo em múltiplos núcleos.

O workload também pode ser distribuído entre múltiplas máquinas para melhorar a escalabilidade da aplicação. Por exemplo, se uma aplicação tem um workload intensivo em E/S, ele pode ser distribuído entre vários servidores para melhorar a capacidade de atendimento ao usuário.

Sendo assim podemos dizer que o workload é a quantidade de trabalho que uma aplicação precisa realizar e é um fator importante na escolha da abordagem correta de concorrência ou paralelismo, desta forma a distribuição adequada do workload pode ajudar a melhorar o desempenho e a escalabilidade da aplicação.

Concorrência em Go é muito poderosa e também simples de godar está foi a intensão dos engenheiros que desenvolveram Go. A resolução de muitos problemas é bem mais eficiente utilizando concorrência e este é o poder de Go, por isto, se tornou um Deus quando o assunto é concorrência em backend. Devido a isto, problemas englobados neste universo serão resolvidos com muita eficiência e o mais importante, com muito pouco recurso computacional.

Exemplo de um Worker em Go:

Todo programa em inicia-se criando o package e importando as libs que irá utilizar.

package main

import "fmt"
import "time"
Enter fullscreen mode Exit fullscreen mode

Esta função é nosso Worker, ele está sendo executado em goroutine de forma concorrente enquanto ele processa o restante do código está também sendo executado.

Vamos entender os parâmetros um pouco os channels, que é a forma de passarmos valores entre goroutines de forma segura.

  • jobs <-chan int: Este é um canal de entrada (receive-only)
  • results chan<- int: Este é um canal de saída (send-only)
  • result2 chan string: Este é um canal bidirecional, isso significa que, dentro da função, você pode tanto enviar quanto receber valores deste canal.
func worker(id int, jobs <-chan int, results chan<- int) {
    for j := range jobs {
        fmt.Println("worker:", id, "started  job", j)
        time.Sleep(time.Second)
        fmt.Println("worker:", id, "finished job", j)
        results <- j * 8
    }
}
Enter fullscreen mode Exit fullscreen mode

Aqui estamos iniciando todo nosso programa declarando os channels.

Disparamos diversas goroutines do nosso worker para que possamos distribuir as cargas de trabalho, temos 3 workers sendo disparados de forma concorrente.

func main() {
        jobs := make(chan int, 100)
    results := make(chan int, 100)
    for w := 1; w <= 3; w++ {
        go worker(w, jobs, results)
    }

    for j := 1; j <= 5; j++ {
        jobs <- j
    }

    close(jobs)
    for a := 1; a <= 5; a++ {
        <-results
    }
}
Enter fullscreen mode Exit fullscreen mode

A linguagem Go é projetada para lidar com concorrência de maneira eficiente e segura. A concorrência é alcançada através de "goroutines", que são pequenos pedaços de código que podem ser executados de forma concorrente e não necessariamente paralela.

Elas são muito leves em termos de recursos, o que significa que é possível criar milhares delas em um único programa sem prejudicar o desempenho.

O runtime do Go é responsável por decidir se uma goroutine será executada de forma concorrente ou em paralelo usando múltiplos núcleos ou não, dependendo da disponibilidade de recursos e do número de núcleos físicos disponíveis.

A comunicação entre goroutines é feita através de "canais", que são mecanismos de sincronização que permitem a transferência de dados de forma segura entre goroutines. Isso permite que as goroutines trabalhem em conjunto, compartilhando dados e cooperando para alcançar um objetivo comum.

O pacote net/http trabalha de forma concorrente escalando as requisições e distribuindo melhor os recursos computacionais.
Se quer visualizar benchmarks sobre este assunto e comparações entre linguagens de programação diferentes da uma conferida neste link benchmark.

Aqui da para ter uma ideia do poder que é o net/http sendo abstraído pelo framework quick desenvolvido em Go que utiliza net/http e comparando com Elixir que é uma linguagem de programação funcional e concorrente.

Elixir vs Quick

Quando falamos de concorrência temos que ressaltar que existe alguns tipos de cargas de trabalhos eles serão nossa bússola para determinar nosso ponto de equilíbrio quando tivermos que resolver problemas evolvendo concorrência.

Uma thread pode fazer dois tipos de cargas de trabalhos (Workloads): CPU-Bound e o IO-Bound.

CPU-BOUND

Esta carga de trabalho nunca irá cria uma situação em que a thread pode ser colocado em estados de espera. Ele estará constantemente fazendo cálculos. Uma thread calculando pi para décima oitava potência seria limitado pela CPU. (curiosidade: Emma Haruka Iwao é uma cientista da computação japonesa e Engenheira de Desenvolvimento Cloud do Google. Em 2019, Haruka Iwao calculou o valor de pi mais preciso do mundo, que incluiu 31,4 trilhões de dígitos). 

Este tipo de carga de trabalho, trabalhos ligadas à CPU, você precisa do paralelismo para aproveitar a simultaneidade, mais Goroutines não irá ajuda-lo e não serão eficientes, podendo atrasar ainda mais as cargas de trabalho a serem executadas, isto ocorre devido ao custo de latência (o tempo gasto) de mover Goroutines dentro e fora da thread do sistema operacional

IO-BOUND

Neste tipo de carga de trabalho as threads entram em estados de espera. Um bom exemplo seria uma solicitação de acesso a um recurso pela rede ou fazer chamadas para o sistema operacional. Uma thread que precisa acessar um banco de dados, eventos de sincronização (mutex, atomic*), todos estes exemplos fazem com que a thread aguarde então poderíamos dizer que são do tipo de trabalho IO-Bound. Neste tipo de carga de trabalho você não precisa de paralelismo para usar a simultaneidade um único core físico já seria suficiente para uma boa execução de várias **Goroutines. As **Goroutines* estão entrando e saindo dos estados de espera como parte de sua carga de trabalho.

Neste tipo de carga de trabalho ter mais Goroutines do que cores físicos pode acelerar a execução porque o custo de latência de mover Goroutines dentro e fora do thread do sistema operacional não está criando um evento. Sua carga de trabalho é naturalmente interrompida e isso permite que uma Goroutine diferente aproveite o mesmo core físico ao invés de permitir que ele fique ocioso.

CONCLUSÃO

A linguagem Go fornece ferramentas poderosas e fáceis de usar para lidar com concorrência, o que a torna uma excelente escolha para aplicações que requerem alta escalabilidade e desempenho.

Desta forma saberemos quando usar mais cores físicos de forma paralelos ou quando iremos precisar somente utilizar concorrência com poucos cores físicos ou somente um core. Em Go os patterns que irão nos ajudar a equilibrar esta equação, existirá momentos que precisaremos de ambos os recursos de carga de trabalho. Quer aprofundar um pouco mais sobre o assunto basta clicar neste link (Simplificando a complexidade “O Inicio”) onde tem um post bem completo.

Top comments (0)