Mudanças entre as edições de "Go: Processos e Concorrência"

De Aulas
 
(4 revisões intermediárias pelo mesmo usuário não estão sendo mostradas)
Linha 2: Linha 2:
  
 
= Concorrência =
 
= Concorrência =
 +
 +
{{tip|Para entendermos melhor essa parte é bom rebuscar fundamentos dobre o [[Processo]] em Sistemas Operacionais.}}
  
 
A linguagem de programação Go trabalha com concorrência usando as <code>gorotines</code>. Elas exencutam funções paralelamente. É mais ou menos o conceito de ''threads'', mas são mais leves, pois o custo da criação de ''gorotines'' é menor do que se comparado a uma ''thread''.
 
A linguagem de programação Go trabalha com concorrência usando as <code>gorotines</code>. Elas exencutam funções paralelamente. É mais ou menos o conceito de ''threads'', mas são mais leves, pois o custo da criação de ''gorotines'' é menor do que se comparado a uma ''thread''.
Linha 7: Linha 9:
 
; Vantagens das Gorotines
 
; Vantagens das Gorotines
  
* Mais leves computacionamento, ocupando menos memória na pilha e a pilha pode alterar seu tamanho conforme a necessidade da aplicação. As ''threads'' possuem tamanho de pilha fixo;
+
* Mais leves computacionamente, ocupando menos memória na pilha e a pilha pode alterar seu tamanho conforme a necessidade da aplicação. As ''threads'' possuem tamanho de pilha fixo;
 
* As ''gorotines'' são multiplexadas para um número menor de ''threads'' do Sistema Operacional. Ou seja, pode haver uma única ''thread'' no programa com várias ''gorotines'' executando. Caso uma ''thread'' seja bloqueada por uma ''goritne'', as outras ''gorotines'' que estão nela podem ser movidas para outra ''thread'';
 
* As ''gorotines'' são multiplexadas para um número menor de ''threads'' do Sistema Operacional. Ou seja, pode haver uma única ''thread'' no programa com várias ''gorotines'' executando. Caso uma ''thread'' seja bloqueada por uma ''goritne'', as outras ''gorotines'' que estão nela podem ser movidas para outra ''thread'';
 
* ''Goroutines'' se comunicam por meio de <code>canais</code>, que previnem que ''race conditions'' aconteçam ao acessar memória compartilhada.
 
* ''Goroutines'' se comunicam por meio de <code>canais</code>, que previnem que ''race conditions'' aconteçam ao acessar memória compartilhada.
Linha 13: Linha 15:
 
Para chamar uma função de forma concorrente no Go, basta chamá-la com a palavra chave '''go''' na frente:
 
Para chamar uma função de forma concorrente no Go, basta chamá-la com a palavra chave '''go''' na frente:
  
<syntaxhighlight lang=go line>
+
<syntaxhighlight lang=go>
 
package main
 
package main
 
   
 
   
Linha 68: Linha 70:
 
Para só então dar sequência e fechar o programa.
 
Para só então dar sequência e fechar o programa.
  
<syntaxhighlight lang=go line>
+
<syntaxhighlight lang=go>
 
package main
 
package main
  
Linha 110: Linha 112:
 
O exemplo abaixo cria n filhos e aguarda eles terminarem. Ele utiliza o <code>sync.WaitGroup</code> para sincronizar os processos.
 
O exemplo abaixo cria n filhos e aguarda eles terminarem. Ele utiliza o <code>sync.WaitGroup</code> para sincronizar os processos.
  
<syntaxhighlight lang=go line>
+
<syntaxhighlight lang=go>
 
package main
 
package main
  
Linha 139: Linha 141:
 
wg.Wait()                    // aguarda todos os filhos terminarem
 
wg.Wait()                    // aguarda todos os filhos terminarem
 
fmt.Println("END PROGRAM...") // imprime na tela
 
fmt.Println("END PROGRAM...") // imprime na tela
}
 
</syntaxhighlight>
 
 
= Comunicação entre Processos =
 
 
A comunicação entre processos no go é feito por '''canais'''. Eles são comparados aos [https://saulo.arisa.com.br/wiki/index.php/Comunica%C3%A7%C3%A3o_entre_Processos_em_C#Programa_pipes.c ''pipes'' na linguagem C]. Cada canal tem um tipo associado. <code>chan T</code> é um canal do tipo T.
 
 
<syntaxhighlight lang=go>
 
// Canal a do tipo int
 
a := make(chan int)
 
</syntaxhighlight>
 
 
Para usar o canal, usamos uma seta direcional
 
 
<syntaxhighlight lang=go>
 
// estamos lendo do canal a e jogando para a variável data
 
data := <- a
 
 
// estamos escrevendo as informações de data para o canal a
 
a <- data
 
</syntaxhighlight>
 
 
Envios e recebimentos de informações pelos canais são '''bloqueantes''', ou seja. Quando você envia algo, a função aguarda até que outra '''gorotine''' receba o dado. Da mesma forma funciona o envio. Isso também é últim para sincronização de '''gorotines'''.
 
 
== Exemplo de comunicação usando canais ==
 
 
<syntaxhighlight lang=go line>
 
package main
 
 
import (
 
"fmt"
 
"time"
 
)
 
 
func child(fc chan string, cc chan string) {
 
msg := <-cc                                  // aguarda a mensagem do pai
 
fmt.Println("CHILD: Father says " + msg)      // imprime na tela
 
time.Sleep(2 * time.Second)                  // aguarda 2 segundos
 
fmt.Println("CHILD: I will response just ok") // imprime na tela
 
fc <- "ok!"                                  // envia a resposta pelo canal do pai
 
}
 
 
func main() {
 
fatherChannel := make(chan string)                      // canal de comunicação do pai
 
childCHannel := make(chan string)                      // canal de comunicação do filho
 
go child(fatherChannel, childCHannel)                  // chama a rotina child paralelamente
 
fmt.Println("FATHER: I will say hello to my child")    // imprime na tela
 
msg := "hello"                                          // cria a variável msg e atribui a string hello
 
childCHannel <- msg                                    // envia a mensagem hello pelo canal do filho
 
msg = <-fatherChannel                                  // aguarda a resposta do filho
 
fmt.Println("FATHER: I receive " + msg + " from child") // imprime na tela
 
}
 
</syntaxhighlight>
 
 
== Canais Unidirecionais ==
 
 
Os canais unidirecionais são apenas para envio ou recebimento. Pode-se converter um bidirecional para unidirecional, mas não o contrário.
 
 
<syntaxhighlight lang=go>
 
sendChannel := make(chan<- string) // canal apenas de envio
 
</syntaxhighlight>
 
 
<syntaxhighlight lang=go>
 
func send(ch chan<- string) // diz que o canal é só de envio
 
</syntaxhighlight>
 
 
== Fechando Canais ==
 
 
<syntaxhighlight lang=go line>
 
package main
 
 
import (
 
"fmt"
 
)
 
 
func child(c chan int) {
 
defer close(c) // Fecha o canal quando terminar a função
 
for i := 0; i < 3; i++ {
 
c <- i // Envia informações algumas vezes pelo canal
 
}
 
}
 
func main() {
 
ch := make(chan int)
 
go child(ch)
 
for {
 
res, ok := <-ch  // res recebe a resposta e ok se o canal está aberto
 
if ok == false { // Se ok é falso, então o canal foi fechado
 
fmt.Println("Canal fechado", ok)
 
break
 
}
 
fmt.Println("Recebi", res, ok) // Vai imprimindo o que recebe
 
}
 
}
 
</syntaxhighlight>
 
 
== Canais com buffer ==
 
 
Os canais com buffer são bloqueados caso a capacidade tenha sido excedida. Por exemplo. Se o seu buffer tem capacidade de 5 inteiros, e você quer enviar 10. Se seu programa não conseguiu fazer qualquer leitura, o próximo envio ficará bloqueado até que libere um espaço.
 
 
<syntaxhighlight lang=go line>
 
package main
 
 
import (
 
"fmt"
 
"time"
 
)
 
 
func child(ch chan int) {
 
defer close(ch)
 
// Já vamos tentar enviar 5, mas somente pode ter 2 inteiros no buffer
 
for i := 0; i < 5; i++ {
 
ch <- i // Caso o buffer esteja cheio, essa linha fica bloqueada
 
fmt.Println("enviei", i, "com sucesso para o canal")
 
}
 
}
 
func main() {
 
// Canal com capacidade de 2 inteiros.
 
ch := make(chan int, 2) //Veja que temos um parâmetro extra
 
go child(ch)
 
time.Sleep(2 * time.Second)
 
for v := range ch {
 
fmt.Println("recebi o valor", v, "do canal")
 
time.Sleep(2 * time.Second)
 
 
}
 
}
 
</syntaxhighlight>
 
 
== Usando select nos canais ==
 
 
podemos usar <code>select</code> (sintaxe tipo o switch-case) para múltiplas operações de enviar e receber dados via canais. O select bloqueia até alguma das operações recebeu algo ou enviou (de forma síncrona). Se mais de uma rotina de mensagem terminou, escolhe entre uma aleatoriamente.
 
 
Veja que no nosso exemplo vai sempre imprimir apenas o resultado do '''child2'''. Isso porque o '''child1''' leva mais tempo para execução e o select pega o primeiro. Daí o programa fecha.
 
 
Ok, mas '''pra que eu usaria isso?'''
 
 
Existem várias situações em que precisamos atividades competitivas, por exemplo testar qual caminho de roteadores até um determinado site está mais rápido. O que retornar primeiro é o escolhido. Ou mesmo imagine que uma tabela de banco de dados está replicada em dois ou mais servidores. Pode-se fazer uma requisição de teste para cada servidor, o que retornar antes será a tabela a ser acessada.
 
 
<syntaxhighlight lang=go line>
 
package main
 
 
import (
 
"fmt"
 
"time"
 
)
 
 
func child1(ch chan string) {
 
time.Sleep(6 * time.Second)
 
ch <- "do child1"
 
}
 
func child2(ch chan string) {
 
time.Sleep(3 * time.Second)
 
ch <- "do child2"
 
 
}
 
func main() {
 
ch1 := make(chan string)
 
ch2 := make(chan string)
 
go child1(ch1)
 
go child2(ch2)
 
// Podemos usar select
 
select {
 
case s1 := <-ch1:
 
fmt.Println(s1)
 
case s2 := <-ch2:
 
fmt.Println(s2)
 
}
 
}
 
</syntaxhighlight>
 
 
= Deadlocks ¯\_(ツ)_/¯ =
 
 
Quando usamos canais, é importante ficarmos atentos para não gerar ''deadlocks''. Se uma ''gorotine'' enviar dados para um canal esperando que outra ''gorotine a receba'', mas essa outra rotina não recebe, o programa entrará em estado de ''deadlock''. Veja o exemplo a baixo:
 
 
<syntaxhighlight lang=go line>
 
package main
 
 
import (
 
"fmt"
 
)
 
 
func child(c chan bool) {
 
a := <-c
 
fmt.Printf("Recebi %t e estou enviando o contrário\n", a)
 
c <- !a
 
}
 
func main() {
 
ch := make(chan bool)
 
go child(ch)
 
response := <-ch
 
fmt.Printf("Acho que nunca vou receber porque não enviei nada %t\n", response)
 
fmt.Println("main function")
 
}
 
</syntaxhighlight>
 
 
se executarmos o programa, ele vai retornar algo como:
 
 
<pre>
 
fatal error: all goroutines are asleep - deadlock!
 
 
goroutine 1 [chan receive]:
 
main.main()
 
        /home/saulo/dev/golang/src/aulas.com/distribuidos/deadlock/deadlock.go:15 +0x85
 
 
goroutine 6 [chan receive]:
 
main.hello(0x0)
 
        /home/saulo/dev/golang/src/aulas.com/distribuidos/deadlock/deadlock.go:8 +0x2c
 
created by main.main
 
        /home/saulo/dev/golang/src/aulas.com/distribuidos/deadlock/deadlock.go:14 +0x6f
 
exit status 2
 
</pre>
 
 
= Exclusão Múltua - Mutex =
 
 
Como mecanismo de bloqueio, as ''gorotines'' utilizam o '''Mutex'''. Ele tenta garantir que apenas uma ''gorotine'' esteja executando a região crítica de um código em um dado momento, evitando que outras ''gorotines'' também tentem. Isso evita que a ''race condition'' ocorra.
 
 
Uma ''gorotine'' pode bloquear e desbloquear uma região crítica. essa região ficará bloqueada até que ela seja desbloqueada.
 
 
; Exemplo:
 
 
<syntaxhighlight lang=go line>
 
package main
 
 
import (
 
"fmt"
 
"sync"
 
)
 
 
// x é uma região crítica
 
var x = 0
 
 
func increment(wg *sync.WaitGroup, m *sync.Mutex) {
 
// Estamos entrando em uma região crítica, então bloqueamos
 
m.Lock()
 
x = x + 1
 
// Ao sair da região crítica desbloqueamos
 
m.Unlock()
 
wg.Done()
 
}
 
func main() {
 
var w sync.WaitGroup
 
var m sync.Mutex
 
for i := 0; i < 1000; i++ {
 
w.Add(1)
 
go increment(&w, &m)
 
}
 
w.Wait()
 
fmt.Println("Valor final de x", x)
 
 
}
 
}
 
</syntaxhighlight>
 
</syntaxhighlight>

Edição atual tal como às 09h58min de 17 de abril de 2024

Afluentes: Sistemas Distribuídos e Mobile

Concorrência

Tplnote Bulbgraph.png

Para entendermos melhor essa parte é bom rebuscar fundamentos dobre o Processo em Sistemas Operacionais.

A linguagem de programação Go trabalha com concorrência usando as gorotines. Elas exencutam funções paralelamente. É mais ou menos o conceito de threads, mas são mais leves, pois o custo da criação de gorotines é menor do que se comparado a uma thread.

Vantagens das Gorotines
  • Mais leves computacionamente, ocupando menos memória na pilha e a pilha pode alterar seu tamanho conforme a necessidade da aplicação. As threads possuem tamanho de pilha fixo;
  • As gorotines são multiplexadas para um número menor de threads do Sistema Operacional. Ou seja, pode haver uma única thread no programa com várias gorotines executando. Caso uma thread seja bloqueada por uma goritne, as outras gorotines que estão nela podem ser movidas para outra thread;
  • Goroutines se comunicam por meio de canais, que previnem que race conditions aconteçam ao acessar memória compartilhada.

Para chamar uma função de forma concorrente no Go, basta chamá-la com a palavra chave go na frente:

package main
 
import (  
    "fmt"
)
 
func hello() {  
    fmt.Println("Hello world goroutine")
}
func main() {  
    go hello()
    // Vamos dar uma pausa aqui senão o programa pode terminar
    // antes de executar a gorotine
    time.Sleep(1 * time.Second)
    fmt.Println("main function")
}

Subprocessos

Estou falando em subprocessos, mas estamos implementando gorotines na linguagem go.

Veja que no exemplo, vamos criar uma gorotine que será filha do processo principal. Nas duas execuções, colocamos algumas linhas de execução e um pequeno delay aleatório.

O problema é que caso o programa termine sem ter terminado a gorotine chamada por ele, ela fecha também. Para contornar esse problema, vamos usar um sistema de sincronização via WAIT.

A gente cria uma variável que será um objeto do tipo sync.WaitGroup e para cada gorotine instanciada, a gente faz um incremento no objeto.

var wg sync.WaitGroup
wg.Add(1)
go child(&wg) // child é nossa função para paralelizar

De dentro da função child, que irá executar em paralelo, a gente já de início dá um defer para executar um Done no objeto WaitGroup. Cada done feito no objeto, é decrementado em um uma variável interna que controla a quantidade de gorotines executando.

defer wg.Done()

Não podemos esquecer que vamos ter que passar o objeto do WaitGroup como parâmetro de referência, então nossa função tem que estar preparada pra isso.

func child(wg *sync.WaitGroup) { //...

Ao final do programa principal, a gente usa um WAIT para agardar até que a gorotine tenha terminado.

wg.Wait()

Para só então dar sequência e fechar o programa.

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

func child(wg *sync.WaitGroup) {
	defer wg.Done()                   // decrementa o contador de processo
	fmt.Println("Luke: Noooooo.....") // imprime na tela
	for i := 0; i < 5; i++ {          // executa o laço 5 vezes
		fmt.Println("Luke: No!")                        // imprime n a tela
		d := rand.Intn(2000)                            // retorna um número randômico de 0 a 1999
		time.Sleep(time.Duration(d) * time.Millisecond) // aguarda um tempo
	}
	fmt.Println("Luke Skywalker falls.") // imprime na tela
}

func main() {
	var wg sync.WaitGroup                           // wg é tilizado para sincronizar os processos
	wg.Add(1)                                       // incrementa o contador de processos
	go child(&wg)                                   // chama a rotina child paralelamente
	fmt.Println("Vader: Luke! I am your father...") // imprime na tela
	for i := 0; i < 5; i++ {                        // executa o laço 5 vezes
		fmt.Println("Vader: Yes!")                      // imprime na tela
		d := rand.Intn(2000)                            // retorna um número randômico de 0 a 1999
		time.Sleep(time.Duration(d) * time.Millisecond) // aguarda um tempo
	}
	wg.Wait()                         // aguarda os subprocessos terminarem
	fmt.Println("Dart Vader leaves.") // imprime na tela
}

Muitos Filhos

Quando um processo principal (pai) cria vários filhos, pode ser necessário aguardar que todos eles terminem seu processamento. Para isso, usamos uma forma de sincronização para o pai aguardar os filhos fecharem.

O exemplo abaixo cria n filhos e aguarda eles terminarem. Ele utiliza o sync.WaitGroup para sincronizar os processos.

package main

import (
	"fmt"
	"math/rand"
	"sync"
	"time"
)

func child(wg *sync.WaitGroup, id int) { // função do filho
	defer wg.Done()          // decrementa o sincronizador
	for i := 0; i < 5; i++ { // conta até 5
		fmt.Println("CHILD[", id, "] ", i)              // imprime i
		d := rand.Intn(2000)                            // pega um valor randômico
		time.Sleep(time.Duration(d) * time.Millisecond) // pausa um tempo aleatório
	}
	fmt.Println("CHILD[", id, "] done...") // imprime a finalização
}

func main() { // programa principal
	fmt.Println("START PROGRAM...") // imprime na tela
	var wg sync.WaitGroup           // inicializa o sincronizador
	for i := 0; i < 5; i++ {        // cria 5 filhos
		wg.Add(1)        // adiciona um no sincronizador
		go child(&wg, i) //cria filho
	}
	wg.Wait()                     // aguarda todos os filhos terminarem
	fmt.Println("END PROGRAM...") // imprime na tela
}

Referências