Entendo Programação Paralela Na Prática (Go)

Vejo pessoal tendo bastante dúvida sobre como funciona a programação paralela e concorrente. Então decidi dar meus 50 centavos de contribuição para o tópico.

Nesse mini artigo vou mostrar e explicar como funciona o paralelismo na prática.

Vale ressaltar que é um exemplo simples e que em cenários em que o paralelismo é realmente exigido as coisas se tornam um pouco mais complexas.
Mas para colocar o pézinho na água deve bastar.

Mas não tão rápido! Aqui vai uma breve introdução sobre o que é esse tal de paralelismo.

Digamos que você esteja tentando arrumar seu quarto bagunçado no fim de semana. A quantidade de roupas bagunçadas, livros e lixo é impressionante, e em vez de um fim de semana, você leva vários dias para terminar de arrumar seu quarto.

Agora suponha que seus irmãos decidiram lhe ajudar com a zona do seu quarto. A tarefa progride muito mais rápido e você é capaz de terminar no prazo proposto. Este princípio é a ideia central por trás da computação paralela. Você pode reduzir drasticamente a computação dividindo uma tarefa grande em tarefas menores que vários processadores podem executar de uma só vez. Com processos paralelos, uma tarefa que normalmente levaria tempo de (n) pode ser reduzida (n/t).

texto adapdato:

https://curc.readthedocs.io/en/latest/programming/parallel-programming-fundamentals.html

Vamos ao que interessa!

package main

import "fmt"

// calcularSoma
// parâmetros:
// inicio e fim são os parâmetros que devem ser "constantes" inicio=0 e fim=250
// isso é para garantir que as threads (goroutines) vão estar fazendo o cálculo na mesma proporção para que a tarefa
// seja igualmente dividia
// já o ch é um "pipe" para que o valor da soma possa ser enviado de volta para a thread main dentro da func main()
func calcularSoma(inicio, fim int, ch chan int) {
	soma := 0
	for i := inicio; i <= fim; i++ {
		soma += i
	}
	ch <- soma // Envia o resultado para o canal
}

func main() {
	n := 1000 // Valor até o qual queremos calcular a soma
	soma := 0

	// Canal para receber os resultados, declarado na thread principal da aplicação
	// Essa é a variável que vai poder transitar entre as threads da aplicação
	ch := make(chan int)

	numGoroutines := 4         // Número de goroutines para dividir o trabalho
	passo := n / numGoroutines // aqui vamos de fato dividir a contagem para cada go routine

	for i := 0; i < numGoroutines; i++ {
		inicio := i*passo + 1  // aqui estamos garantindo que o início da contagem seja sempre 0
		fim := (i + 1) * passo // aqui estamos garantindo que o fim da contagem seja sempre o valor de passo

		// como queremos triggar nossa goroutine 4 vezes como declarado na variável numGoroutines
		// aqui garantimos que ela vai ser triggada 4 vezes mesmo
		if i == numGoroutines-1 {
			fim = n
		}

		// aqui é onde a magica acontece.
		// podemos entender uma goroutine em termos simplistas como uma thread isolada, isto é, que não está
		// compartilhando memória com as demais threads dentro deste processo ou PID.
		// O que garante que iremos conseguir paralelizar o processo vai ser o ch (chan)
		// que é um tipo de dado especial do Go, e ele sim vai conseguir transitar entre
		// a thread que o ch foi enviado e a thread principal
		go calcularSoma(inicio, fim, ch) // Inicia uma goroutine para cada intervalo
	}

	// Aguarda as goroutines finalizarem e recebe os resultados do canal
	for i := 0; i < numGoroutines; i++ {
		somaParcial := <-ch // Recebe o resultado do canal
		soma += somaParcial
	}

	fmt.Println("A soma de todos os números até", n, "é:", soma)
}

// Esse é o mesmo código sem o uso de goroutines e channels

//package main
//
//import "fmt"
//
//func main() {
//	n := 1000 // Valor até o qual queremos calcular a soma
//	soma := 0
//
//	for i := 1; i <= n; i++ {
//		soma += i
//	}
//
//	fmt.Println("A soma de todos os números até", n, "é:", soma)
//}

Nesse código todo comentado basicamente estamos dividindo igualmente a tarefa de somar os números entre 4 threads. E isso é chamado de paralelismo por que a contagem esta sendo feita por 4 threads executando "ao mesmo tempo" e basicamente o mais importante é que elas dependem umas das outras para exibir o resultado final.

Isso é garantido nesse trecho de código.

// Aguarda as goroutines finalizarem e recebe os resultados do canal
	for i := 0; i < numGoroutines; i++ {
		somaParcial := <-ch // Recebe o resultado do canal
		soma += somaParcial
	}

Bem... ao final quando as 4 threads fizerem sua parte o resultado é exibido.

That's all, folks! Espero ter ajudado a entender um poquinho sobre esse assunto :)

Só pra causar um pouco de caos na discussão sobre paralelismo:

Em 2011, um usuário do 4chan publicou o Sleep Sort. A ideia é vc criar uma thread pra cada elemento do seu vetor, e fazê-la dormir por um tempo proporcional ao valor do elemento. Aí, é só pedir pra cada thread imprimir o seu valor quando voltar a executar.

Aqui o vai o código (que eu copiei direto do link acima), pra quem se interessar:

#!/bin/bash
function f() {
    sleep "$1"
    echo "$1"
}
while [ -n "$1" ]
do
    f "$1" &
    shift
done
wait

example usage:
./sleepsort.bash 5 3 6 3 6 3 1 4 7

Pra quem está começando: esse exemplo não é nada útil em termos de como ordenar um vetor. Mas, ainda assim, ele dá uma saída correta xD

irado!! kkk 🤩🤩