1. Code
  2. Coding Fundamentals

Vamos Lá: Paralelismo em Golang, Parte 1

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.
Scroll to top
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:

1
--- Run sequentially as normal functions
2
1
3
2
4
3
5
1
6
2
7
3
8
1
9
2
10
3
11
1
12
2
13
3

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:

1
--- Run concurrently as goroutines
2
2
3
2
4
3
5
1
6
3
7
2
8
1
9
3
10
1
11
1
12
3
13
2
14
2
15
1
16
3

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.

1
package main
2
3
import (
4
    "fmt"
5
  "time"
6
	"math/rand"
7
)
8
9
var r = rand.New(rand.NewSource(time.Now().UnixNano()))
10
11
func f(s string) {
12
	// Sleep up to half a second

13
	delay := time.Duration(r.Int() % 500) * time.Millisecond
14
	time.Sleep(delay)
15
	fmt.Println(s)	
16
}
17
18
19
func main() {
20
	fmt.Println("--- Run sequentially as normal functions")
21
	for i := 0; i < 4; i++ {
22
		f("1")
23
		f("2")
24
		f("3")
25
	}
26
27
	fmt.Println("--- Run concurrently as goroutines")
28
	for i := 0; i < 5; i++ {
29
		go f("1")
30
		go f("2")
31
		go f("3")
32
	}
33
	
34
	// Wait for 6 more seconds to let all go routine finish

35
	time.Sleep(time.Duration(6) * time.Second)
36
	fmt.Println("--- Done.")	
37
}

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.

1
package main
2
3
import (
4
    "fmt"
5
	"time"
6
	"math/rand"
7
	"sync"
8
)
9
10
var r = rand.New(rand.NewSource(time.Now().UnixNano()))
11
var wg sync.WaitGroup
12
13
func f(s string) {
14
	defer wg.Done()
15
	// Sleep up to half a second

16
	delay := time.Duration(r.Int() % 500) * time.Millisecond
17
	time.Sleep(delay)
18
	fmt.Println(s)	
19
}
20
21
22
func main() {
23
	fmt.Println("--- Run sequentially as normal functions")
24
	for i := 0; i < 4; i++ {
25
		wg.Add(3)
26
		f("1")
27
		f("2")
28
		f("3")
29
		
30
	}
31
32
	fmt.Println("--- Run concurrently as goroutines")
33
	for i := 0; i < 5; i++ {
34
		wg.Add(3)
35
		go f("1")
36
		go f("2")
37
		go f("3")
38
	}
39
	
40
	wg.Wait()	
41
}

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

1
var fetchedUrls = struct {
2
    urls map[string]bool
3
	m    sync.Mutex
4
}{urls: make(map[string]bool)}

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

1
// Check if this url has already been fetched (or being fetched)

2
    fetchedUrls.m.Lock()
3
	if fetchedUrls.urls[url] {
4
		fetchedUrls.m.Unlock()
5
		return
6
	}
7
8
	// OK. Let's fetch this url

9
	fetchedUrls.urls[url] = true
10
	fetchedUrls.m.Unlock()

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