Aula 27 - Tutorial Golang - Sincronização de canais
Fontes usadas para esse post:
Canais tem uma forma um pouco diferente de
receber e
enviar dados.
ch <- values // enviando em um canal
value := <- ch // Recebendo em um canal e atribuindo o valor
<-ch // Recebendo em um canal e descartando o resultado
close(ch) // Fechando um canal. Isso significa que o canal não pode enviar nenhum dado
Os
channels funcionam parecido com as
threads tradicionais, mas, ao invés das
threads serem geradas pelo
schedule do
sistema operacional, elas são geradas pela
runtime do
Go.
O
runtime são os códigos das bibliotecas
Go.
Então, quando a gente compila um
código Go, o
runtime dele é compilado junto com o código, juntando os códigos do runtime com o código do nosso programa.
Parecido com o que ocorre com a
JVM do java e o próprio código de um programa em java, que roda dentro do ambiente virtual
JVM.
O pessoal que criou o
golang, para simplificar o uso de fluxos de rotinas concorrentes, ou seja, as
threads, resolveu criar o próprio
gerenciador de tarefas, isto é, o
schedule, que criam as próprias
threads.
Por isso quem gerencia as threads em
Go é a própria
runtime.
Essas
threads são chamadas de
threads de
nível de usuário, ou t
erra de usuário, ou
threads verdes ou ainda
threads leves.
Cada thread go ocupa
2kb de memória, cada thread tradicional ocupa
1 mega.
Schedule
O
Schedule das
threads tradicionais, ou seja, o cara responsável pelo cronograma de tarefas dentro do
SO, pode usar o
escalonamento preemptivo ou
cooperativo.
O
preemptivo é o que define um tempo de processamento para cada tarefa, não importando se a tarefa foi ou não concluída, o processamento é interrompido em função de outra tarefa.
O
cooperativo, a tarefa é processada o máximo possível, só liberando o processador caso termine de executar, as tarefas gerenciam seu próprio ciclo de vida.
Depois de ser atribuído a um
worker, cabe à tarefa se ela deve ou não ser liberada de um
trabalhador, ou seja, devolver o controle ao
Schedule.
O trabalho do agendador é apenas atribuir tarefas a qualquer
worker que esteja livre.
Go usa por padrão o escalonamento
cooperativo, mas também é possível usar o escalonamento
preemptivo.
Esse escalonamento pode levar um processo a monopolizar os recursos de processamento.
Para evitar isso, o Go estabelece algumas regras, por exemplo, se tiver que acessar disco, ou fazer qualquer outra ação não bloqueante, vai para a próxima
goroutine que precisa ser processada.
Ou se, por exemplo, tiver que fazer uma chamada externa a uma API, também faz o processamento de outra
thread (
goroutine), se tiver que dá um timeout ou um sleep, mesma coisa.
Channel synchronization
Os
canais podem ser usados para sincronizar
goroutines.
Um
canal pode fazer uma
goroutine esperar até terminar.
O
canal pode então ser usado para notificar uma
2ª goroutine.
Imagine que você tenha várias
goroutines.
Às vezes, uma
goroutine precisa ser concluída antes que você possa iniciar a próxima (síncrona).
Isso pode ser resolvido com
canais.
Podemos usar canais para sincronizar a execução entre essas
goroutines.
Aqui está um exemplo de uso de um recebimento de bloqueio para esperar que uma
goroutine termine.
Para aguardar a conclusão de várias
goroutines, você pode usar um
WaitGroup.
Esta é a função que vamos executar em uma
goroutine.
O
canal done será notificado que o trabalho da função foi terminado.
func task(done chan bool) {
fmt.Print("working...")
time.Sleep(time.Second)
fmt.Println("done")
done <- true
}
Na
main(), é inicializada uma
goroutine chamada
task, dando a ela o
canal done que recebe a
notificação.
O código
<-done,
bloqueia até receber uma notificação do
task no canal
done.
func main() {
done := make(chan bool, 1)
go task(done)
<-done
}
Se você remover a linha
<-done do código, o programa seria encerrado antes mesmo da
task iniciar.
Código completo
package main
import (
"fmt"
"time"
)
func task(done chan bool) {
fmt.Print("working...")
time.Sleep(time.Second)
fmt.Println("done")
done <- true
}
func main() {
done := make(chan bool, 1)
go task(done)
<-done
}
Execute o programa com:
go run channel_synchronization.go
Saída:
working...done
Código 02 completo
package main
import (
"fmt"
"time"
)
func task(taskId int, msg chan int) {
for res := range msg {
fmt.Println("Task: ", taskId, "Message: ", res)
time.Sleep(time.Second)
}
}
func main() {
msg := make(chan int)
go task(1, msg)
//go task(2, msg)
//go task(3, msg)
//go task(4, msg)
for i := 0; i < 10; i++ {
msg <- i
}
}
Saída:
Task: 1 Message: 0
Task: 1 Message: 1
Task: 1 Message: 2
Task: 1 Message: 3
Task: 1 Message: 4
Task: 1 Message: 5
Task: 1 Message: 6
Task: 1 Message: 7
Task: 1 Message: 8
Task: 1 Message: 9
Sincronizando goroutines
Dá pra fazer uma
goroutine esperar por
outra.
Você pode fazer isso com uma instrução
if e lendo o valor do canal.
No código abaixo, a
goroutine task2 aguardará a finalização da goroutine
task.
Observação
As goroutines (
task,
task2) podem ser síncronas o tempo todo enquanto são executadas simultaneamente.
Como tudo é concorrente, você ainda pode usar o
thread main do seu programa ao mesmo tempo.
package main
import "fmt"
import "time"
func task(done chan bool) {
fmt.Print("Task 1 (goroutine) running...")
time.Sleep(time.Second)
fmt.Println("done")
done <- true
}
func task2() {
fmt.Println("Task 2 (goroutine)")
}
func main() {
done := make(chan bool, 1)
go task(done)
fmt.Println("Im in the main thread!")
if <- done {
go task2()
fmt.Scanln()
}
}
Executando o programa com:
go run channel_synchronization.go
Você verá a saída abaixo e o programa vai ficar esperando que você tecle qualquer coisa para encerrar, por causa do
fmt.scanln().
Saída:
Im in the main thread!
Task 1 (goroutine) running...done
Task 2 (goroutine)
É isso pessoal, fico por aqui!
Até mais. :)
Meus links de afiliados:
Obrigado e bons estudos. ;)