Mudanças entre as edições de "Go: Comunicação entre Processos"
(5 revisões intermediárias pelo mesmo usuário não estão sendo mostradas) | |||
Linha 81: | Linha 81: | ||
} | } | ||
} | } | ||
+ | |||
func main() { | func main() { | ||
ch := make(chan int) | ch := make(chan int) | ||
go child(ch) | go child(ch) | ||
for { | for { | ||
− | res, ok := <-ch | + | res, ok := <-ch // res recebe a resposta e ok se o canal está aberto |
− | if ok | + | if !ok { // Se ok é falso, então o canal foi fechado |
− | fmt.Println(" | + | fmt.Println("O canal foi fechado. canal:", ok) |
break | break | ||
} | } | ||
− | fmt.Println("Recebi", res, ok) // Vai imprimindo o que recebe | + | fmt.Println("Recebi", res, "canal:", ok) // Vai imprimindo o que recebe |
} | } | ||
} | } | ||
Linha 186: | Linha 187: | ||
c <- !a | c <- !a | ||
} | } | ||
+ | |||
func main() { | func main() { | ||
ch := make(chan bool) | ch := make(chan bool) | ||
Linha 228: | Linha 230: | ||
) | ) | ||
− | // x é uma região crítica | + | var x = 0 // x é uma região crítica |
− | |||
func increment(wg *sync.WaitGroup, m *sync.Mutex) { | func increment(wg *sync.WaitGroup, m *sync.Mutex) { | ||
− | // | + | m.Lock() // Entramos em uma região crítica, então bloqueamos |
− | |||
x = x + 1 | x = x + 1 | ||
− | // Ao sair da região crítica desbloqueamos | + | m.Unlock() // Ao sair da região crítica desbloqueamos |
− | |||
wg.Done() | wg.Done() | ||
} | } | ||
+ | |||
func main() { | func main() { | ||
var w sync.WaitGroup | var w sync.WaitGroup | ||
Linha 260: | Linha 260: | ||
* Distribua o processamento para ''n'' processos filhos | * Distribua o processamento para ''n'' processos filhos | ||
* Ao receber todas as respostas, junte elas e apresente ao usuário. | * Ao receber todas as respostas, junte elas e apresente ao usuário. | ||
+ | * Todos os processos filhos e o processo pai devem usar um arquivo log para gravar informações do que estão fazendo. | ||
'''OBSERVAÇÕES''': Todos os processos poderão utilizar o arquivo de log para armazenar informações. Contudo, o abrir, gravar e fechar o arquivo de log é uma região crítica e só pode ser acessada por apenas um processo. Os outros só poderão acessar essa região crítica quando nenhum outro processo estiver acessando. | '''OBSERVAÇÕES''': Todos os processos poderão utilizar o arquivo de log para armazenar informações. Contudo, o abrir, gravar e fechar o arquivo de log é uma região crítica e só pode ser acessada por apenas um processo. Os outros só poderão acessar essa região crítica quando nenhum outro processo estiver acessando. | ||
− | |||
− | |||
− | |||
− |
Edição atual tal como às 20h36min de 24 de abril de 2024
Afluentes: Sistemas Distribuídos e Mobile
Comunicação entre Processos
|
A comunicação entre processos no go é feito por canais. Eles são comparados aos pipes na linguagem C. Cada canal tem um tipo associado. chan T
é um canal do tipo T.
// Canal a do tipo int
a := make(chan int)
Para usar o canal, usamos uma seta direcional
// 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
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
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
}
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.
sendChannel := make(chan<- string) // canal apenas de envio
func send(ch chan<- string) // diz que o canal é só de envio
Fechando Canais
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 { // Se ok é falso, então o canal foi fechado
fmt.Println("O canal foi fechado. canal:", ok)
break
}
fmt.Println("Recebi", res, "canal:", ok) // Vai imprimindo o que recebe
}
}
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.
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)
}
}
Usando select nos canais
podemos usar select
(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.
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)
}
}
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:
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")
}
se executarmos o programa, ele vai retornar algo como:
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
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
package main
import (
"fmt"
"sync"
)
var x = 0 // x é uma região crítica
func increment(wg *sync.WaitGroup, m *sync.Mutex) {
m.Lock() // Entramos em uma região crítica, então bloqueamos
x = x + 1
m.Unlock() // Ao sair da região crítica desbloqueamos
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)
}
Atividade
Agora vamos brincar um pouco com processamento concorrente. Para isso, vamos criar uma aplicação concorrente em grupos de até 4 pessoas ou individual.
DEFINIÇÃO:
- Criar uma aplicação que resolve um conjunto de cálculos (operação com matrizes, análise de sinais, criptografia simples, ordenação, etc.);
- Distribua o processamento para n processos filhos
- Ao receber todas as respostas, junte elas e apresente ao usuário.
- Todos os processos filhos e o processo pai devem usar um arquivo log para gravar informações do que estão fazendo.
OBSERVAÇÕES: Todos os processos poderão utilizar o arquivo de log para armazenar informações. Contudo, o abrir, gravar e fechar o arquivo de log é uma região crítica e só pode ser acessada por apenas um processo. Os outros só poderão acessar essa região crítica quando nenhum outro processo estiver acessando.