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

Vamos Lá: Paralelismo em Golang, Parte 1

by
Difficulty:IntermediateLength:ShortLanguages:
This post is part of a series called Let's Go: Golang Concurrency.
Let's Go: Golang Concurrency, Part 2

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

Visão Geral

Toda linguagem de programação bem sucedida tem um recurso matador. O forte de Go é a programação paralela. Ela foi projetada com base num modelo teórico forte (PSC) e provê sintaxe a nível de linguagem na forma da palavra chave "go" que inicia uma tarefa assíncrona (sim, a linguagem se chama assim por isso) bem como uma forma embutida de comunicar-se entre tarefas simultâneas.

Nessa primeira parte, apresentaremos o modelo PSC, que é implementado pela Go, goroutines e como sincronizar a operação de múltiplas goroutines em cooperação. Na próxima parte, falaremos sobre canais em Go e como coordenar goroutines sem estruturas de dados sincronizadas.

PSC

PSC significa Processo Sequencial de Comunicação. Foi apresentado por Tony (C. A. R.) Hoare em 1978. PSC é uma framework de alto nível para descrição de sistemas concorrentes. É muito mais fácil criar programas paralelos corretamente operando em nível PSC de abstração que com o uso típico de linhas (threads) e travas (locks).

Goroutines

Goroutines é uma jogo com coroutines. Mas, não são o mesmo. Goroutine é uma função executada em uma linha separada da linha que a invocou, para não bloqueá-la. Várias goroutines podem compartilhar a mesma linha de OS. Ao contrário de coroutines, goroutines não podem passar controle, de forma explicita, para outra. O tempo de execução de Go o faz, implicitamente, quando uma goroutine bloquear acesso de E/S.

Vejamos algum código. O programa Go abaixo define uma função, chamada de "f", que dorme aleatoriamente até meio segundo e, então, imprime seu argumento. A função main() invoca f() em um laço de quatro iterações, e em cada iteração invoca f() três vezes com "1", "2" e "3", em sequênica. Como esperado, retorna:

Então, main chama f() como uma goroutine em um laço similar. Agora, o resultado é diferente porque o tempo de execução de Go executará as goroutines f paralelamente e, pela dormida aleatória, os valores impressão não aparecem na ordem que f() foi invocada. Eis o retorno:

O programa em si usa os pacotes "time" e "math/rand", da biblioteca padrão, para implementar durmida aleatória e esperar o fim de todas as gorotinas. Isso é importante porque quando a linha principal termina, o programa finaliza, mesmo que tenhamos goroutines executando.

Grupo de Sincronização

Ao termos várias goroutines executando por todos os lugares, qureremos saber quando elas todas estão prontas.

Existem formas diferentes de fazer isso, mas uma das melhores é usar um WaitGroup. Um WaitGroup é um tipo definido no pacote "sync" que provê as operações Add(), Done() e Wait(). Ele funciona como um contador de quantas goroutines ainda estão ativas e espera até todas terminarem. Sempre que iniciarmos novas goroutines, adicionamos com Add(1) (pode-se adicionar mais que 1 se lançarmos várias goroutines). Quando uma goroutina termina, ela chama Done(), reduzindo em um o contador, e Wait() bloqueia até o contador chegar a zero.

Convertamos o exemplo anterior e o usemos WaitGroup ao invés de dormir por seis segundos. Note que a f() usa defer wg.Done() ao invés de wg.Done() diretamente. Isso é útil para garantir que wg.Done() sempre seja invocada, mesmo que haja um problema e a goroutine encerre antes. Caso contrário, o contador nunca chegará a zero e wg.Wait() bloqueará para sempre.

Outra pequena dica é invocar wg.Add(3) apenas uma vez antes de invocar f() três vezes. Note que invocamos wg.Add() mesmo quando invocamos f() como uma função normal. Isso é necessário por f() chamar wg.Done() não importa se executada como função ou goroutine.

Estruturas de Dados Sincronizadas

As goroutines no programa 1,2,3 não se comunicam nem operam sobre estruturas de dados compartilhados. No mundo real, geralmente isso é preciso. O pacote "sync" provê o tipo Mutex com os métodos Lock() e Unlock() provendo exclusão mútua. Um ótimo exemplo é o mapa padrão de Go.

Não é sincronizado por padrão. Isso significa que se múltiplas goroutines acessarem o mesmo mapa paralelamente sem sincronização externa, os resultados serão imprevisíveis. Mas, se as goroutines concordarem num mutex compartilhado antes de cada acesso e liberá-lo depois, o acesso será serializado.

Tudo Junto

Agora, tudo junto. O Tour do Go tem um exercício para construir um rastreador web. Eles proveem um ótimo framework com um buscador e resultados simulados, para forcar no problema em questão. Recomendamos bastante resolvê-lo sozinho.

Criamos soluções usando duas abordagens: com mapa sincronizado e com canais. O código fonte está disponível aqui.

Eis o mais relevante da solução sincronizada. Primeiro, criemos um mapa com uma estrutura mutex para guardar as URLs buscadas. Notemos a interessante sintaxe onde um tipo anônimo é criado, inicializado e atribuído a uma variável de uma vez só.

Agora, o código pode travar o mutex m antes de acessar o mapa de URL e liberá-lo ao terminar.

Não é totalmente seguro porque qualquer um pode acessar a variável fetchedUrls e esquecer de travar e liberar. Um projeto mais robusto proverá uma estrutura de dados que suporte operações seguras, travando/liberando automaticamente.

Conclusão

Go tem um suporte excelente a paralelismo com as leves goroutines. É muito mais fácil que as linhas tradicionais. Quando é preciso acesso sincronizado para compartilhar estruturas de dados, Go ajuda com sync.Mutex.

Tem muito mais que isso no paralelismo em Go. Fique ligado...

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.