Este artigo é a parte 9 de 11 na série Golang

Muitas vezes se confunde concorrência com paralelismo, no entanto são coisas bastantes distintas. Um dos criadores da linguagem Go pontuou em uma de suas apresentações:

“Concorrência é sobre lidar com várias coisas ao mesmo tempo e paralelismo é sobre fazer várias coisas ao mesmo tempo.” – Rob Pike na conferência Heroku’s Waza intitulada Concurrency is not parallelism

O diagrama abaixo ilustra os processos de Concorrência e Paralelismo em um sistema com um e dois núcleos que executam apenas uma thread por núcleo[fn]Um núcleo é geralmente a unidade de computação básica da CPU, podendo executar um único contexto de programa (ou vários se ele suportar threads de hardware, como no caso de hyperthreading nos processadores da Intel), mantendo o estado corre do programa, registradores, e a correta ordem de execução, e executar as operações através de ALUs (arithmetic logic unit, ou unidade de lógica aritmética)… Discussão no StackOverflow – Difference between core and processor.[/fn].

Concorrência, Paralelismo e Concorrência + Paralelismo em uma máquina com 1 a 2 núcleos. As bolinhas de diferentes cores representam os processos/aplicativos sendo executados.

Na concorrência os processos 1 e 2 são executados de forma concorrente, ou seja:

  • processo 1 executa por alguns ciclos de máquina;
  • depois para e o processo 2 passa a ser executado por alguns ciclos;
  • processo 2 para e retorna a execução no processo 1;

Desta forma um único núcleo pode executar diversos processos/aplicativos além dos processo padrões do sistema, como acesso ao teclado, mouse, execução de uma música, vídeo, …

Já o paralelismo necessita de ao menos dois núcleos de processamento, já que neste caso os processos são de fato executados simultaneamente. As máquinas modernas trabalham tanto com concorrência como paralelismos e, tipicamente, com CPUs de 2 a 8 núcleos os quais podem executar até duas threads por núcleo, o que seria semelhante a ter 4 a 16 núcleos na CPU.

A discussão sobre paralelismo e concorrência é extensa e se passa por estratégias diferentes empregadas na forma de aplicar a concorrência, gerenciamento do acesso escrita e leitura em dispositivos diversos (memórias, disco, dispositivo de media, rede, …) a fim de melhor utilizar os recursos disponíveis no sistema. Tem alguns bons artigos na internet sobre o assunto os quais indico:

Goroutines

Uma goroutine é uma função que pode ser executada de forma concorrente com outras goroutines. Para criar uma goroutine basta adicionar a palavra chave go à frente da função invocada.

No código a seguir três goroutine são iniciadas chamando a mesma função, function, com diferentes argumentos:

package main

import (
	"fmt"
	"time"
)

func function(id int, t time.Duration) {
	for i := 0; i < 10; i++ {
		time.Sleep(t)
		fmt.Printf("Function %02d: %d\n", id, i)
	}
}

func main() {
	fmt.Println("Starting...")

	go function(1, time.Second*1)
	go function(2, time.Second*1)
	go function(3, time.Second*1)

	time.Sleep(time.Second * 5)
	fmt.Println("Ending...")
}

O código em sim é bem simples, a função function apenas aguarda por t segundos e depois imprime a mensagem “Function <ID>: <INTERAÇÃO>“, e este processo se repete por dez vezes no terminal, pelo for da linha 9. Na função main, após imprimir uma mensagem de inicialização, abre três goroutine nas linhas 18, 19 e 20 com as IDs 1, 2 e 3, apenas para identificar qual goroutine está escrevendo no terminal. A linha 22 faz um sleep de 5 segundos para dar tempo das goroutines executarem algumas vezes.

Executando este código no terminal deve retornar algo como:

Bash

alves@arabel:goroutine$ go run goroutine.go
Starting…
Function 03: 0
Function 02: 0
Function 01: 0
Function 03: 1
Function 01: 1
Function 02: 1
Function 03: 2
Function 01: 2
Function 02: 2
Function 03: 3
Function 02: 3
Function 01: 3
Ending…

O sleep da linha 22 é fundamental para que algumas goroutines possam ser executadas, de outra forma o programa termina parando as goroutines. Observe que apenas os 4 primeiros laços das goroutines foram executados, como monstra a saída do código acima. Isto aconteceu porque o Sleep da linha 22 foi de apenas 5 segundos.

Outro ponto importante a observar é que a ordem de execução das goroutine não é a ordem de chamada. Isto ocorre porque as goroutines são enviadas ao sistema como threads do programa principal e são executadas pelos núcleos disponíveis em concorrência com os demais processos do sistema, executando-os de forma assíncrona.

Pacote sync.WaitGroup

O pacote sync fornece uma struct WaitGroup que adiciona uma trava às goroutines impedindo que o código prossiga até que estas sejam liberadas. A struct WaitGroup possui um contador interno e três métodos para controlar as goroutines, apresentadas na sequência.

Método Add

func (wg * WaitGroup ) Add(delta int )

Este método adiciona um valor o delta ao contador do WaitGroup. A cada Done() este contador é decrementado, e quando este chegar a zero todas as travas Wait são liberadas. O valor adicionado ao contador do WaitGroup pode ser negativo, no entanto isto ativa um Add panics.

Método Done

func (wg *WaitGroup) Done()

Este método simplesmente decrementa o contador do WaitGroup por 1 a cada chamada.

Método Wait

func (wg * WaitGroup ) Wait()

Este método simplesmente aguarda até que o contador do WaitGroup seja nulo para prosseguir o processamento.

Para ver o sync.WaitGroup em funcionamento altere o código anterior como segue:

 package main

import (
	"fmt"
	"sync"
	"time"
)

var wg sync.WaitGroup

func function(index int, t time.Duration) {
	defer wg.Done()
	for i := 0; i < 5; i++ {
		time.Sleep(t)
		fmt.Printf("Function %02d: %d\n", index, i)
	}
}

func main() {
	const maxGo = 3

	wg.Add(maxGo)
	fmt.Println("Starting...")

	for i := 0; i < maxGo; i++ {
		go function(i, time.Second*1)
	}

	fmt.Println("Waiting...")
	wg.Wait()
	fmt.Println("Ending...")
}

A primeira alteração é a declaração da linha 9, onde wg é instanciada como um sync.WaitGroup. No início da função function, linha 12, foi adicionado um defer wg.Done(). O comando defer instrui a função a executar o comando que o segue, neste caso um wg.one(), no final da função.

No corpo da função main fiz algumas mudanças para chamar a função function num laço for, linha 25, ao invés de chamadas diretas. Para isto criei a constante maxGo para estabelecer o número de funções a serem chamadas.

A linha 22 configura o contador da WaitGroup para o número de funções a serem chamadas com um wg.Add(naxGo).

A última alteração foi a adição da linha 30, o wg.Wait(), para aguardar até que o contador do WaitGroup chegue a zero para terminar a execução do código.

Neste código o wg foi declarado no bloco principal do código, mas poderia ser declarado na função main e passado a função function por meio de um parâmetro. Neste caso o wg deve ser passado como ponteiro para que o contador interno possa decrementado ou irá gera um erro

fatal error: all goroutines are asleep - deadlock! 

ao retornar pelo wg.Wait().

Pacote sync.Mutex

Concorrência é uma grande evolução na otimização do emprego dos recursos de uma máquina no entanto alguns cuidados devem ser considerados para que a interrupção de execução de um código não gere algumas surpresas.

Considere um código de uma agência bancária que executa diversas operações de crédito e débito em uma conta. Em meio ao processamento este código pode ser interrompido em uma operação de depósito, enquanto uma nova thread com um debito toma prioridade, levando a conta a um saldo negativo.

Normalmente uma pequena tolerância no tempo de saldo negativo poderia resolver o problema e depois numa conta não é, geralmente, esperado alterações tão intensas a ponto de gerar grandes transtorno com isto… Independente do problema ser relevante ou não o fato é que ele existe e deve ser considerado em um sistema de alta confiabilidade.

package main

import (
	"fmt"
	"log"
	"time"
)

type BankAccount struct {
	Balance int
}

func (b *BankAccount) Stress(value int) {
	b.Balance += value
	time.Sleep(time.Microsecond)
	b.Balance -= value

	balance := b.Balance
	fmt.Printf("Balance: %6d\n", balance)
	if balance < 0 {
		log.Fatalf("Balance shoud not be less that zero: %d\n", balance)
	}
}

func main() {
	account := BankAccount{Balance: 100}

	for i := 0; i < 10000; i++ {
		go account.Stress(1000)
	}
}

Para ilustrar considere um operação de escrita em uma conta que acrescenta $1000, e após alguns milissegundos de processamento remover os $1000. Não importando a quantidade que esta operação ocorra o esperado é que o saldo da conta consultado seja sempre o mesmo. Experimento o código a seguir e observe o seu retorno em algumas execuções:

A conta em questão está definida na struct BankAccount, nas linhas 9 a 11, com apenas o atributo Balance para manter o saldo da conta. A função de Stress da conta, linhas 13 a 23, apenas cria a situação de stress de acesso de escrita na conta, executando um depósito de value, linha 14, uma operação de 1 milissegundos, linha 15, e depois um saque de value, linha 17.

As demais linhas são apenas para acessar o saldo da conta e imprimir seu valor. O esperado é que o saldo permaneça o mesmo já que o value acrescido é removido da conta 1 milissegundo depois, em situações convencionais.

No entanto, na função main são invocadas 10.000 chamadas à função Stress como uma goroutine e o resultado observado é muita variância intensa no salto:

Bash


Balance: 1100
Balance: 2100
Balance: 11100
Balance: 4100
Balance: 1100
Balance: 3100
Balance: 1100
2022/06/04 22:08:48 Balance shoud not be less that zero: -900
exit status 1

[Done] exited with code=1 in 0.134 seconds

Como a adição de value ocorre antes de subtração a predominância é de saldo positivo na conta. Caso o código não para por um erro fatal apenas o execute mais algumas vezes até que o erro ocorra.

Uma forma de resolver este problema da concorrência é o emprego do tipo sync.Mutex para cria uma fechadura, Lock(), que bloqueia uma chamada posterior da um goroutine no ponto do Lock(), até que um Unlock() libere a execução.

Método Lock()

func (m *Mutex) Lock()

Lock fecha m. Se a fechadura já estiver em uso a goroutine até que o bloco seja destrancado.

Médoto Unlock()

func (m *Mutex) Unlock()

Unlock destranca m. Gera um run-time error se m já estiver destrancada

Para implementar estas fechaduras efetue três adições ao código anterior, como segue:

package main

import (
	"fmt"
	"log"
	"sync"
	"time"
)

var mutex sync.Mutex

type BankAccount struct {
	Balance int
}

func (b *BankAccount) Stress(value int) {
	mutex.Lock()
	defer mutex.Unlock()

	b.Balance += value
	time.Sleep(time.Microsecond)
	b.Balance -= value

	balance := b.Balance
	fmt.Printf("Balance: %6d\n", balance)
	if balance < 0 {
		log.Fatalf("Balance shoud not be less that zero: %d\n", balance)
	}
}

func main() {
	account := BankAccount{Balance: 100}

	for i := 0; i < 10000; i++ {
		go account.Stress(1000)
	}
}

A primeira adição é a linha 10, onde cria a variável mutex do tipo sync.Mutex. Em seguida a linha 17 adiciona o Lock(), trancando no início da função Stress uma subsequente chamada da função. A última alteração é o defer, na linha 18, que adicionar um Unlock() para a saída da função Stress, removendo a fechadura.

Um segunda execução deste código mostrará um saldo inalterado:

Bash


Balance: 100
Balance: 100
Balance: 100
Balance: 100
Balance: 100
Balance: 100
Balance: 100
Balance: 100

[Done] exited with code=0 in 0.115 seconds

O jantar dos Filósofos

Um outro problema bem conhecido da concorrência é o Dining philosophers problem em que um grupo de filósofos se reúnem em um jantar onde é servido um tipo de espaguete que deve ser comido com dois garfos, no entanto o número de grafos disponíveis é exatamente igual ao número de filósofos. Desta forma eles decidem por um garfo entre cada dois filósofos e a cada momento alguns filósofos iram comer, usando os garfos entre seus vizinho enquanto os demais aguardam o seu momento.

Este problema gera um impasse, se em algum momento cada filósofos pegarem um dos garfos e desta forma nenhum deles consegue comer. O Mutex ajuda a resolver este problema mas nem sempre.

O código a seguir implementa o Jantar dos Filósofos:

package main

import (
	"fmt"
	"sync"
	"time"
)

var eatWaitGroup sync.WaitGroup

type fork struct {
	sync.Mutex
}

type Philosopher struct {
	name                string
	leftName, rightName string
	leftFork, rightFork *fork
	sleepTime           time.Duration
}

func (p Philosopher) Dining() {
	defer eatWaitGroup.Done()
	for i := 0; i < 100; i++ {
		fmt.Printf("(%s) try to eat the forkful %d.\n", p.name, i)
		p.leftFork.Lock()
		fmt.Printf("(%s) grab (%s) fork.\n", p.name, p.leftName)
		p.rightFork.Lock()
		fmt.Printf("(%s) grab (%s) fork.\n", p.name, p.rightName)

		fmt.Printf("(%s) start to eat the forkful %d.\n", p.name, i)
		time.Sleep(p.sleepTime)

		p.leftFork.Unlock()
		fmt.Printf("(%s) drop (%s) fork.\n", p.name, p.leftName)
		p.rightFork.Unlock()
		fmt.Printf("(%s) drop (%s) fork.\n", p.name, p.rightName)

		fmt.Printf("(%s) finish to eat the forkful %d.\n", p.name, i)
	}
}

func main() {
	var (
		philNames = []string{
			"A",
			"B",
			"C",
			// "D",
			// "E",
		}
		forks        []*fork
		philosophers []Philosopher
		nPhil        int = len(philNames)
	)

	for i := 0; i < nPhil; i++ {
		forks = append(forks, new(fork))
	}

	left, right := 0, 0
	for i := 0; i < nPhil; i++ {
		left = (i + nPhil - 1) % nPhil
		right = (i + 1) % nPhil

		philosophers = append(philosophers,
			Philosopher{
				name:      philNames[i],
				leftName:  philNames[left],
				rightName: philNames[right],
				leftFork:  forks[i],
				rightFork: forks[right],
				// sleepTime: time.Microsecond,
				sleepTime: time.Second,
			})

		eatWaitGroup.Add(1)
		go philosophers[i].Dining()
	}

	eatWaitGroup.Wait()
}

Poderia ter empregado o código anterior para fazer a implementação, mas preferi mudar um pouco a estrutura geral para exercitar outras características da linguagem. O eatWaitGroup, definido na linha 9, vai controlar as instâncias das goroutines em execução, para garantir que os filósofos terminem seus almoços. Como antes um Done() deve ser gerado a cada final de execução da goroutine, feito pelo defer na linha 23, e a adição de cada filósofo a mesa é feita por um Add() na linha 77. O eatWaitGroup.Wait(), da linha 81, aguarda que todos tenham almoçado antes de terminar o programa.

Detalhando um pouco mais função Dining, linhas 22 à 41, cada prato será consumido por 100 grafadas, contadas no laço for da linha 24 à 40. A cada grafada o filósofo da ira pegar o grafo à sua esquerda, linha 26, em seguida pega o garfo da direita, linha 28, para depois comer, anunciado na linha 31 e com um intervalo de tempo dado pelo Sleep na linha 21. Depois disto o filósofo coloca o garfo esquerdo na mesa e depois o direito, terminando a sua garfada.

Na função main a variável philNames, definido nas linhas 45 à 51, armazena a lista de nomes dos filósofos e o seu comprimento serve como limitador para os acentos à mesa e garfos disponíveis, linha 54. A slice forks armazena ponteiros para os garfos entre os filósofos, construídos no laço for das linhas 57 à 59. Neste laço é empregado a função new() para criar ponteiros da struct fork, declarada nas linhas 11 à 13 com sync.Mutex‘s. Esta função é bem útil para gerar ponteiros de tipos structs, mas não de tipos primitivos como inteiros.

A linha 53 declara uma slice para a lista de filósofos, tipo Philosopher declarado nas linhas 15 à 20. O tipo Philosopher terá como atributos: name com o nome do filósofo; leftName e rightName como os nomes dos filósofos sentados a sua esquerda e a sua direita, respectivamente; leftFork e rightFork como os ponteiros para os garfos entre o filósofo e outro a sua esquerda e a sua direita, respectivamente; sleepTime um time.Duration para armazenar o tempo de duração de sua garfada.

A alimentação da lista de filósofos ocorre nas linhas 61 à 75, sendo as primeiras linhas apenas para gerar os índices para os filósofos à esquerda e à direita, variáveis left e right, onde o filósofo à esquerda do primeiro é o último da lista. Os índices dos garfos, forks, começam em 0 como o garfo entre o primeiro e o último e 1 o garfo entre o primeiro e o segundo.

Executando este código com um SleepTime grande, 1 segundo, os filósofos almoção sem problemas, pois o tempo de cada garfada é muito grande comparado ao quantum de tempo em cada thread gerando certo sincronismos entre as garfadas.

Bash

alves@arabel:~$ go run deadlock.go
(C) try to eat the forkful 0.
(C) grab (B) fork.
(C) grab (A) fork.
(C) start to eat the forkful 0.
(B) try to eat the forkful 0.

(A) try to eat the forkful 99.
(A) grab (C) fork.
(A) grab (B) fork.
(A) start to eat the forkful 99.
(A) drop (C) fork.
(A) drop (B) fork.
(A) finish to eat the forkful 99.
alves@arabel:~$

No entanto se baixar o SleepTime para algo como 1 milissegundos a chance de que todos os filósofos acabem com um garfo na mão aumenta substancialmente. Experimente remover o comentário da linha 73 e comentar a linha 74. Após isto execute novamente o código e observe o que acontece:

Bash

alves@arabel:~$ go run deadlock.go
(C) try to eat the forkful 0.
(C) grab (B) fork.
(C) grab (A) fork.
(C) start to eat the forkful 0.
(C) drop (B) fork.

(B) drop (C) fork.
(B) finish to eat the forkful 11.
(B) try to eat the forkful 12.
(B) grab (A) fork.
(A) grab (C) fork.
(C) grab (B) fork.
fatal error: all goroutines are asleep – deadlock!

goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc00007c140?)

sync.(*Mutex).Lock(…)
/usr/local/lib/go/src/sync/mutex.go:81
main.Philosopher.Dining({{0x497257, 0x1}, {0x497256, 0x1}, {0x497255, 0x1}, 0xc0000140f8, 0xc0000140d8, 0x3e8})
/home/alves/Documents/Estudos/Go/go/goroutine/deadlock/deadlock.go:28 +0x355
created by main.main
/home/alves/Documents/Estudos/Go/go/goroutine/deadlock/deadlock.go:81 +0x16d
exit status 2
alves@arabel:~$

Caso o código execute normalmente, o que pode acontecer por um acaso, execute novamente o código mais algumas vezes que um erro deve ocorrer. Este erro é chamado de deadlock, onde todas as goroutines estão paralisadas, o que ocorre quanto todos os filósofos estão com um garfo em sua posse.

Deadlock é um problema bem conhecido com algumas soluções já propostas como a solução de Dijkstra que emprega um semáforo e uma variável de estado por filósofo; solução de hierarquia; entre muitas outras[fn]C560 Lecture notes — Dining Philosophers[/fn].

Vou empregar aqui uma solução mais simplista, sem criar novas variáveis, semáforos, lista de precedências, … Mas antes de implementar isto veja último método da struck Mutex.

Método TryLock()

func (m *Mutex) TryLock() bool

Este método tenta trancar m e retorna true se for bem sucedido. Observe que, embora existam usos corretos do TryLock, eles são raros, e o uso do TryLock geralmente é um sinal de um problema mais profundo em um uso específico de mutexes.

Embora a solução que vá empregar aqui não seja a forma mais eficiente de implementar uma solução para o problema, me parece suficientemente elegante, deixando ao próprio sistema a decisão de priorizar a thread conforme a conveniência.

A ideia é colocar em um laço infinito a pegada dos garfos da mesa, pegando o esquerdo, como antes, e usar o TryLock() para tentar pegar o segundo garfo, o rightFork, e caso este não esteja disponível por o garfo esquerdo novamente na mesa e aguardar um antes de pegar novamente o garfo esquerdo e tentar novamente pegar o direito. O laço prossegue até que ambos os garfos estejam à mão e o filósofo possa comer sua garfada.

A mudança no código ocorre apenas no método Dining(), apresentado abaixo:

...
func (p Philosopher) Dining() {
	defer eatWaitGroup.Done()
	for i := 0; i < 100; i++ {
		fmt.Printf("(%s) try to eat the forkful %d.\n", p.name, i)
		for {
			p.leftFork.Lock()
			fmt.Printf("(%s) grab (%s) fork.\n", p.name, p.leftName)
			if !p.rightFork.TryLock() {
				fmt.Printf("(%s) drop (%s) fork.\n", p.name, p.leftName)
				p.leftFork.Unlock()
			} else {
				break
			}
			time.Sleep(p.sleepTime)
		}

		fmt.Printf("(%s) start to eat the forkful %d.\n", p.name, i)
		time.Sleep(p.sleepTime)

		p.leftFork.Unlock()
		fmt.Printf("(%s) drop (%s) fork.\n", p.name, p.leftName)
		p.rightFork.Unlock()
		fmt.Printf("(%s) drop (%s) fork.\n", p.name, p.rightName)

		fmt.Printf("(%s) finish to eat the forkful %d.\n", p.name, i)
	}
}
...

A mudança no código são apenas das linhas 4 à 16, o laço for infinito. A saída é pelo break na linha 13, após o garfo direito ser pego. O Sleep na linha 15 foi necessário para o código não ficar preso num laço infinito largando o grafo e pegando-o logo em seguida. Execute novamente o código e alimente os filósofos:

Bash

(C) try to eat the forkful 0.
(C) grab (B) fork.
(C) start to eat the forkful 0.
(C) drop (B) fork.

(A) drop (B) fork.
(A) finish to eat the forkful 98.
(A) try to eat the forkful 99.
(A) grab (C) fork.
(A) start to eat the forkful 99.
(A) drop (C) fork.
(A) drop (B) fork.
(A) finish to eat the forkful 99.
alves@arabel:~$

Numa análise bem superficial, aparentemente os filósofos B e C iniciam com alguma vantagem, deixando o A para terminar comento sozinho por algumas garfadas finais. Para uma aplicação mais robusta é razoável fazer testes mais informativos, no entanto atende os propósitos do momento.

Pacote sync.Once

Existem situações que que se deseja que uma classe seja instanciada apenas uma única vez em todo o código, este padrão é conhecido como Singleton pattern e pode ser implementado no Go com a struct Once, declarada no pacote sync.

Existem muitas formas de implementar Singleton pattern em Go e aqui vamos explorar algumas destas possibilidades.

Imagine que seu aplicativo tenha de acessar a um banco de dados e a sua abertura deve ocorre apenas uma vez. Em um código concorrente o desafio pode ser tornar ainda mais complexo, visto que uma chamada ao banco de dados pode ocorrer de diversas formar em diferentes pontos do código, e em todas elas o seu banco de dados tem de estar conectado, o que deve ocorrer apenas uma única vez.

O código abaixo implementa um modelo para simular a conexão de um banco feito de forma concorrente por diversas vezes, onde apenas uma deve ser bem sucedida:

package main

import (
	"fmt"
	"sync"
	"time"
)

type dbConnection struct{}

var (
	connecton *dbConnection
	wg        sync.WaitGroup
)

func openDBConnection(id int) {
	defer wg.Done()
	if connecton == nil {
		// some process...
		//time.Sleep(time.Millisecond)
		// finish process
		connecton = &dbConnection{}
		fmt.Printf("Make a database connection (%d)...\n", id)
	}

	fmt.Printf("Done (%d)...\n", id)
}

func main() {
	for id := 0; id < 5; id++ {
		wg.Add(1)
		go openDBConnection(id)
	}

	wg.Wait()
}

A instância do banco de dados ocorre na linha 22 e o if da linha 18 testa se o ponteiro para um banco de dados, connection, aponta para algo diferente de nil, ou seja se o banco de dados foi instanciado.

Em geral, na forma como está, este código deve funcionar mesmo rodando em concorrência, gerando algo como:

Make a database connection (4)...
Done (4)...
Done (0)...
Done (1)...
Done (2)...
Done (3)...

No entanto, se adicionar algum processamento antes de abrir a conexão ao banco de dados, o que pode ser feito removendo o comentário do time.Sleep da linha 20, a concorrência poderá fazer com que o código seja parado em diferentes pontos dentro do if, neste caso a linha time.Sleep, e tentar instanciar o banco de dados diversas vezes:

Make a database connection (3)...
Make a database connection (4)...
Done (4)...
Make a database connection (1)...
Done (1)...
Done (3)...
Make a database connection (2)...
Make a database connection (0)...
Done (0)...
Done (2)...

Se não funcionar com 1 milissegundo aumente o Sleep para 1 segundo deve ser suficiente. Isto ocorre porque várias instâncias da goroutine adentraram o if e pararam na linha 20, o time.Sleep como havia anunciado acima.

Uma possível correção é adicionar um Mutex Lock() e um defer Unlock() antes do if:

...
var (
	connecton *dbConnection
	wg        sync.WaitGroup
	mutex     sync.Mutex
)

func openDBConnection(id int) {
	mutex.Lock()
	defer wg.Done()
	defer mutex.Unlock()
	if connecton == nil {
		// some process...
		time.Sleep(time.Millisecond)
		// finish process
		connecton = &dbConnection{}
		fmt.Printf("Make a database connection (%d)...\n", id)
	}

	fmt.Printf("Done (%d)...\n", id)
}
...

Isto garante que o if será testado uma única vez até que o banco de dados seja instanciado. Embora esta solução implemente Singleton pattern no objeto connection muito bem, o mutex.Lock() e Unlock() e o if serão executados todas as vezes que a função openDBConnection for invocada, consumindo recursos de processamento desnecessários.

Uma forma mais simples e elegante de fazer isto é por meio de uma struct Once, empregando o seu método .Do(f func).

Método Do

func (o *Once) Do(f func())

Este método chama pela função f somente se esta esta sendo chamada pela primeira vez. Veja o exemplo no código a seguir:

package main

import (
	"fmt"
	"sync"
)

var once sync.Once

func f() {
	fmt.Println("Running...")
}

func main() {
	for i := 1; i < 100; i++ {
		once.Do(f)
	}
}

Ainda que invocada 100 vezes pelo laço for, linhas 15 à 17, apenas um “Running...” será impresso, ou seja a função f() será executada apenas uma vez pelo comando once.Do(f).

Implementando um once.Do no código anterior o Singleton pattern ficará bem mais simples:

package main

import (
	"fmt"
	"sync"
	"time"
)

type dbConnection struct{}

var (
	connecton *dbConnection
	wg        sync.WaitGroup
	once      sync.Once
)

func openDBConnection(id int) {
	defer wg.Done()

	once.Do(func() {
		// some process...
		time.Sleep(time.Microsecond)
		// finish process
		connecton = &dbConnection{}
		fmt.Printf("Make a database connection (%d)...\n", id)
	})

	fmt.Printf("Done (%d)...\n", id)
}

func main() {
	for id := 0; id < 5; id++ {
		wg.Add(1)
		go openDBConnection(id)
	}

	wg.Wait()
}

Aqui o once.Do está invocando uma função anônima, definida nas linhas 20 a 26, o que é bem conveniente visto que está será invocada apenas uma única vez e de dentro da função openDBConnection. O Mutex não será mais necessário, pois uma vez que a função anônima em once.Do começa a ser executada nenhuma outra pode ser iniciada.

O time.Sleep foi ajustado a 1 milissegundo, no entanto pode ser usado qualquer outro valor e ainda assim apenas uma instância do connection será criada, já que o once.Do executa apenas uma vez.

Considerações Finais

Quando iniciei a escrever este artigo imaginava que apresentaria as goroutines centrado nas discussões sobre canais com, no máximo, uma ou outra chamada às structs do pacote sync. Entretanto não consegui sair do pacote sync o qual me surpreendeu e ainda tem muito para explorar.

O pacote sync provem primitivas básicas para sincronização como execução mutua, travas além dos tipos Once e WaitGroup, algumas exploradas neste artigo. A sincronização feita por meio de canais e comunicação devo explorar num próximo artigo.

Como última colocação deixo dois artigos que inspiraram na composição deste: