Advertisement
  1. Code
  2. Go

Vamos Lá: Paralelismo em Golang, Parte 2

Scroll to top
Read Time: 9 min
This post is part of a series called Let's Go: Golang Concurrency.
Let's Go: Golang Concurrency, Part 1

Portuguese (Português) translation by Erick Patrick (you can also view the original English article)

Visão Geral

Uma dos recursos únicos da Go é o uso de canais para comunicação segura entre goroutines. Nesse artigo, aprenderemos o que são canais, como usá-los efetivamente e alguns padrões comuns.

O Que É Um Canal?

Um canal é uma fila sincronizada em memória que goroutines e funções padrões podem usar para enviar e receber valores tipados. Comunicação é seralizada pelo canal.

Podemos cria rum canal usando make() especificando o tipo de valores que o canal aceita:

ch := make(chan int)

Go provê a sintaxe de de flexa para enviar para e receber de canais:

1
    // send value to a channel

2
    ch <- 5
3
4
    // receive value from a channel

5
    x := <- ch

Não temos de consumir o valor. Tudo bem removermos um valor do canal:

<-ch

Canais são bloqueantes por padrão. Se enviarmos um valor a umcanal, o bloquearemos até ele ser recebido. Igualmente, se recebermos de um canal, bloquearemo-no até alguém enviar um valor para o canal.

O programa a seguir demonstra isso. A função main() cria um canal e começa uma goroutine que imprime "start", lê um valor do canal e o imprime também. main() começa outra goroutine que imprime um traço ("-") a cada segundo. Então, dorme por 2.5 segundos, enviar um valor para o canal e dorme mais 3 segundos para deixar todas goroutines terminarem.

1
import (
2
    "fmt"
3
    "time"
4
)
5
6
func main() {
7
    ch := make(chan int)
8
9
    // Start a goroutine that reads a value from a channel and prints it

10
    go func(ch chan int) {
11
        fmt.Println("start")
12
        fmt.Println(<-ch)
13
    }(ch)
14
15
    // Start a goroutine that prints a dash every second

16
    go func() {
17
        for i := 0; i < 5; i++ {
18
            time.Sleep(time.Second)
19
            fmt.Println("-")
20
        }
21
    }()
22
23
    // Sleep for two seconds

24
    time.Sleep(2500 * time.Millisecond)
25
26
    // Send a value to the channel

27
    ch <- 5
28
29
    // Sleep three more seconds to let all goroutines finish

30
    time.Sleep(3 * time.Second)
31
}

Esse programa demonstra muito bem a natureza bloqueante do canal. A primeira goroutine imprime "start" de primeira, mas e bloqueada na tentativa de receber do canal até a função main(), que dorme por 2.5s, envie o valor. A outra goroutine apenas provê uma indicação visual do fluxo de tempo, imprimindo um traço a cada segundo.

Eis o retorno:

1
start
2
-
3
-
4
5
5
-
6
-
7
-

Canais de Dados Temporários

Esse comportamento liga, fortemente, remetente e recebedor, e, algumas vezes, não é o que queremos. Go provê vários mecanismos para resolver isso.

Canais de dados temporários são canais que guardam um certo número (pré-definido) de valores que o remetente não bloqueia até que esteja cheio, mesmo que ninguém receba.

Para criar um canal de dados temporário, apenas adicione a capacidade como segundo argumento:

ch := make(chan int, 5)

O programa a seguir ilustra o comportamento de canais de dados temporários: main() define um desse canais com capacidade 3. Então começa uma goroutine que lê os dados tmeporários do canal a cada segundo e imprime-os, e outr goroutine que apenas imprime um traço a cada segundo, dando indicação visual de progressão de tempo. Então, ele envia sinco valores ao canal.

1
import (
2
    "fmt"
3
    "time"
4
)
5
6
7
func main() {
8
    ch := make(chan int, 3)
9
10
    // Start a goroutine that reads a value from the channel every second and prints it

11
    go func(ch chan int) {
12
        for {
13
            time.Sleep(time.Second)
14
            fmt.Printf("Goroutine received: %d\n", <-ch)
15
        }
16
17
    }(ch)
18
19
    // Start a goroutine that prints a dash every second

20
    go func() {
21
        for i := 0; i < 5; i++ {
22
            time.Sleep(time.Second)
23
            fmt.Println("-")
24
        }
25
    }()
26
27
    // Push values to the channel as fast as possible

28
    for i := 0; i < 5; i++ {
29
        ch <- i
30
        fmt.Printf("main() pushed: %d\n", i)
31
    }
32
33
    // Sleep five more seconds to let all goroutines finish

34
    time.Sleep(5 * time.Second)
35
}

O que aconteceu em tempo de execução? Os 3 primeiros valores foram salvos temporariamente pelo canal, de forma imediata, e main() bloqueou. Após um segundo, o valor é recebido pela goroutine e main() pode enviar outro valor. Outro segundo passa e a goroutina recebe outro valor e main() pode enviar o último valor. Nessa altura, a goroutine continua recebendo valores do canal a cada segundo.

Eis o retorno:

1
main() pushed: 0
2
main() pushed: 1
3
main() pushed: 2
4
-
5
Goroutine received: 0
6
main() pushed: 3
7
-
8
Goroutine received: 1
9
main() pushed: 4
10
-
11
Goroutine received: 2
12
-
13
Goroutine received: 3
14
-
15
Goroutine received: 4

Seleção

Canais de dados temporário podem (desde que haja espaço) resolver o problema de flutuações temporárias onde não há receptor suficiente para processar todas as mensagens enviadas. Mas há, também, o problem contrário de receptores bloqueados esperando mensagens para processar. Go tem a solução.

E se quisermos que a goroutine faça algo quando não houver mensagens a processar no canal? Um bom exemplo é se o receptor esperar por mensagens de múltiplos canais. Não queremos bloquear o canal A se o canal B tiver mensagens. O programa a seguir tenta computar a soma de 3 e 5 usando o poder total da máquina.

A ideia é simular uma operação complexa (exemplo: consulta remota a BD distribuído) com redundância. A função sum() (note sua definição aninhada em main()) aceita dois parâmetros inteiros e retorna um canal de inteiros. Uma goroutine interna e anônima dorme por tempo aleatório menor que 1 segundo e, então, escreve a soma no canal, fecha e retorna-o.

Agora, main invoca sum(3,5) por quatro vezes e salva os canais resultantes em variáveis, ch1 a ch4. As quatro invocações retornam imediatamente porque o sono aleatório acontece dentro da goroutine que cada sum() invoca.

Eis a parte legal. A declaração select permitr main() esperar por todos os canais e responder o primeiro que retornar algo. A declaração select opera como uma declaração switch.

1
func main() {
2
    r := rand.New(rand.NewSource(time.Now().UnixNano()))
3
4
    sum := func(a int, b int) <-chan int {
5
        ch := make(chan int)
6
        go func() {
7
            // Random time up to one second

8
            delay := time.Duration(r.Int()%1000) * time.Millisecond
9
            time.Sleep(delay)
10
            ch <- a + b
11
            close(ch)
12
        }()
13
        return ch
14
    }
15
16
    // Call sum 4 times with the same parameters

17
    ch1 := sum(3, 5)
18
    ch2 := sum(3, 5)
19
    ch3 := sum(3, 5)
20
    ch4 := sum(3, 5)
21
22
    // wait for the first goroutine to write to its channel

23
    select {
24
    case result := <-ch1:
25
        fmt.Printf("ch1: 3 + 5 = %d", result)
26
    case result := <-ch2:
27
        fmt.Printf("ch2: 3 + 5 = %d", result)
28
    case result := <-ch3:
29
        fmt.Printf("ch3: 3 + 5 = %d", result)
30
    case result := <-ch4:
31
        fmt.Printf("ch4: 3 + 5 = %d", result)
32
    }
33
}

Por vezes, não queremos main() bloqueada, nem que seja pela primeira goroutine a terminar. Aqui, podemos usar um caso padrão que executará se todos os canais estão bloqueados.

Exemplo de Rastreador Web

No artigo anterior, mostramos uma solução de um exercício de rastreador web do Tour de Go. Usamos goroutines e mapas sincronizados. Também resolvemos o exercício usando canais. O código completo das soluções está disponível no GitHub.

As partes relevantes: Primeiro, uma estrutura que será enviada ao canal sempre que uma goroutina analisar uma página. Ela contém a profunidade atual e todas as URLs da página.

1
type links struct {
2
    urls  []string
3
    depth int
4
}

A função fetchURL() aceita uma URL, profundidade e canal de saída. Ela usa o buscador (provido pelo exercício) para obter URLs de todos os links da página. Ela envia a lista de URL como mensagem única ao canal candidato como uma estrutura links com profundidade menor. A profundidade representa o quanto se deverá rastrear. Quando chegar a 0, não é preciso mais processamento.

1
func fetchURL(url string, depth int, candidates chan links) {
2
    body, urls, err := fetcher.Fetch(url)
3
    fmt.Printf("found: %s %q\n", url, body)
4
5
    if err != nil {
6
        fmt.Println(err)
7
    }
8
9
    candidates <- links{urls, depth - 1}
10
}

A função ChannelCrawl() coordena tudo. Ela registra todas as URL que já foram buscadas em um mapa. Não é preciso sincronizar o acesso porque nenhuma outra função ou goroutina usará. Também define um canal candidato em que todas as goroutines escreverão seus resultados.

Então, começa a invocar parseUrl como goroutines para cada nova URL. A lógica registra quantas goroutines foram invocadas, usando um contador. Sempre que um valor é lido do canal, o contador é reduzido (a goroutine remetente termina após o envio), e sempre que uma nova goroutine é lançada, o contador é incrementado. Se profundidade chegar a zero, nenhuma outra goroutine será invocada e a função principal, continuará lendo do canal até todas goroutines terminarem.

1
// ChannelCrawl crawls links from a seed url

2
func ChannelCrawl(url string, depth int, fetcher Fetcher) {
3
    candidates := make(chan links, 0)
4
    fetched := make(map[string]bool)
5
    counter := 1
6
7
    // Fetch initial url to seed the candidates channel

8
    go fetchURL(url, depth, candidates)
9
10
    for counter > 0 {
11
        candidateLinks := <-candidates
12
        counter--
13
        depth = candidateLinks.depth
14
        for _, candidate := range candidateLinks.urls {
15
            // Already fetched. Continue...

16
            if fetched[candidate] {
17
                continue
18
            }
19
20
            // Add to fetched mapped

21
            fetched[candidate] = true
22
23
            if depth > 0 {
24
                counter++
25
                go fetchURL(candidate, depth, candidates)
26
            }
27
        }
28
    }

Conclusão

Os canais da Go proveem várias opções para comunicação segura entre goroutines. O suporte à sintaxe é tanto concisa quanto ilustrativa. É um benefício real ao criar algortimos concorrentes. Há muitos mais sobre canais que o mostrado aqui. Encorajamos a ver mais e familiarizar-se com os vários padrões de paralelismo que eles permitem.

Advertisement
Did you find this post useful?
Want a weekly email summary?
Subscribe below and we’ll send you a weekly email summary of all new Code tutorials. Never miss out on learning about the next big thing.
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.