Diz-se que Go é a linguagem C do século XXI. Eu acho que existem duas razões: primeiro, o Go é uma linguagem simples; segundo, a simultaneidade é um tema importante no mundo atual, e o Go suporta esse recurso no nível da linguagem.
goroutines e simultaneidade são incorporados ao design central do Go. Eles são semelhantes aos tópicos, mas funcionam de maneira diferente. Mais de uma dúzia de goroutines talvez tenham apenas 5 ou 6 threads subjacentes. Go também lhe dá suporte total para compartilhar memória em seus goroutines. Uma goroutine geralmente usa 4 ~ 5 KB de memória de pilha. Portanto, não é difícil executar milhares de goroutines em um único computador. Uma goroutine é mais leve, mais eficiente e mais conveniente que os threads do sistema.
Os goroutines são executados no gerenciador de encadeamentos em tempo de execução no Go. Usamos a palavra-chave go
para criar uma nova goroutine, que é uma função no nível subjacente (*** main () é uma goroutine ***).
go hello(a, b, c)
Vamos ao exemplo:
package main
import (
"fmt"
"runtime"
)
func say(s string) {
for i := 0; i < 5; i++ {
runtime.Gosched()
fmt.Println(s)
}
}
func main() {
go say("world") // create a new goroutine
say("hello") // current goroutine
}
Retorno
hello
world
hello
world
hello
world
hello
world
hello
Vemos que é muito fácil usar a simultaneidade no Go usando a palavra-chave go
. No exemplo acima, essas duas goroutines compartilham alguma memória, mas seria melhor seguir a receita de design: Não use dados compartilhados para se comunicar, use a comunicação para compartilhar dados.
runtime.Gosched () significa deixar a CPU executar outras goroutines e voltar em algum momento.
O agendador usa apenas um thread para executar todos os goroutines, o que significa que ele apenas implementa a simultaneidade. Se você deseja usar mais núcleos de CPU para aproveitar o processamento paralelo, é necessário chamar runtime.GOMAXPROCS (n) para definir o número de núcleos que deseja usar. Se n <1
, nada muda. Esta função pode ser removida no futuro, veja mais detalhes sobre o processamento paralelo e simultaneidade neste artigo(em inglês).
Os goroutines são executados no mesmo espaço de endereço de memória, portanto, você precisa manter a sincronização quando quiser acessar a memória compartilhada. Como você se comunica entre diferentes goroutines? Go usa um mecanismo de comunicação muito bom chamado channel
. channel
é como um pipeline bidirecional em shells Unix: use channel
para enviar ou receber dados. O único tipo de dado que pode ser usado em canais é o tipo channel
e a palavra-chave chan
. Esteja ciente de que você tem que usar make
para criar um novo channel
.
ci := make(chan int)
cs := make(chan string)
cf := make(chan interface{})
canais usa, o operador <-
para enviar ou receber dados.
ch <- v // envia v para o canal ch.
v := <-ch // recebe dados de ch, e os assina em v
Exemplos:
package main
import "fmt"
func sum(a []int, c chan int) {
total := 0
for _, v := range a {
total += v
}
c <- total // envia o total para c
}
func main() {
a := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
go sum(a[:len(a)/2], c)
go sum(a[len(a)/2:], c)
x, y := <-c, <-c // recebe de c
fmt.Println(x, y, x + y)
}
Enviando e recebendo dados em blocos de canais por padrão, é muito mais fácil usar goroutines síncronas. O que quero dizer com block é que uma goroutine não continuará ao receber dados de um canal vazio, ou seja, (value: = <-ch
), até que outras goroutines enviem dados para este canal. Por outro lado, a goroutine não continuará até que os dados enviados a um canal, ou seja (ch <-5
), sejam recebidos.
Eu introduzi canais não-bufferizados acima. Go também tem canais em buffer que podem armazenar mais de um único elemento. Por exemplo, ch: = make (chan bool, 4)
, aqui criamos um canal que pode armazenar 4 elementos booleanos. Assim, neste canal, podemos enviar 4 elementos sem bloqueio, mas a goroutine será bloqueada quando você tentar enviar um quinto elemento e nenhuma goroutine o receber.
ch := make(chan type, n)
n == 0 ! non-buffer(block)
n > 0 ! buffer(non-block until n elements in the channel)
Você pode tentar o seguinte código no seu computador e alterar alguns valores.
package main
import "fmt"
func main() {
c := make(chan int, 2) // altera de 2 para 1 e retornará um erro, mas 3 funciona
c <- 1
c <- 2
fmt.Println(<-c)
fmt.Println(<-c)
}
Podemos usar o range para operar em canais buffer como em slice e map.
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 1, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x + y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
for i := range c
não parará de ler dados do canal até que o canal seja fechado. Usamos a palavra-chave close
para fechar o canal no exemplo acima. É impossível enviar ou receber dados em um canal fechado; você pode usar v, ok: = <-ch
para testar se um canal está fechado. Se ok
retornar falso, significa que não há dados nesse canal e foi fechado.
Lembre-se sempre de fechar os canais nos produtores e não nos consumidores, ou é muito fácil entrar em status de pânico.
Outra coisa que você precisa lembrar é que os canais não são como arquivos. Você não precisa fechá-los com frequência, a menos que tenha certeza de que o canal é completamente inútil ou deseja sair de loops de intervalo.
Nos exemplos acima, usamos apenas um canal, mas como podemos lidar com mais de um canal? Go tem uma palavra-chave chamada select
para ouvir muitos canais.
select
está bloqueando por padrão e continua a executar somente quando um dos canais tem dados para enviar ou receber. Se vários canais estiverem prontos para usar ao mesmo tempo, selecione a opção para executar aleatoriamente.
package main
import "fmt"
func fibonacci(c, quit chan int) {
x, y := 1, 1
for {
select {
case c <- x:
x, y = y, x + y
case <-quit:
fmt.Println("quit")
return
}
}
}
func main() {
c := make(chan int)
quit := make(chan int)
go func() {
for i := 0; i < 10; i++ {
fmt.Println(<-c)
}
quit <- 0
}()
fibonacci(c, quit)
}
select
tem um caso default
, assim como switch
. Quando todos os canais não estão prontos para uso, ele executa o caso padrão (ele não aguarda mais o canal).
select {
case i := <-c:
// use i
default:
// Executa aqui quando C estiver bloqueado
}
Às vezes uma goroutine fica bloqueada. Como podemos evitar isso para evitar que todo o programa bloqueie? É simples, podemos definir um tempo limite no select.
func main() {
c := make(chan int)
o := make(chan bool)
go func() {
for {
select {
case v := <- c:
println(v)
case <- time.After(5 * time.Second):
println("timeout")
o <- true
break
}
}
}()
<- o
}
O pacote runtime
tem algumas funções para lidar com goroutines.
runtime.Goexit ()
Sai da gorout atual, mas as funções adiadas serão executadas como de costume.
runtime.Gosched ()
Permite que o planejador execute outras goroutines e volte em algum momento.
runtime.NumCPU () int
Retorna o número de núcleos da CPU
runtime.NumGoroutine () int
Retorna o número de goroutines
runtime.GOMAXPROCS (n int) int
Define quantos núcleos de CPU você deseja usar
- Prefácio
- Seção anterior: interfaces
- Próxima seção: Summary