Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Go
Code

Vamos Lá: Paralelismo em Golang, Parte 2

by
Difficulty:IntermediateLength:MediumLanguages:
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:

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.

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:

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.

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:

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.

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.

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.

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.

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
Advertisement
Advertisement
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.