Golang – 09. Goroutines – Concorrência e Paralelismo
- Golang – 01. Introdução
- Golang – 02. Tipos Básicos
- Golang – 03. Structs e Funções e Métodos
- Golang – 04. Estruturas de Controle
- Golang – 05. Gerenciando Pacotes
- Golang – 06. Biblioteca Padrão I – fmt e strings
- Golang – 07. Biblioteca Padrão II – os, os/exec e os/user
- Golang – 08. Interface
- Golang – 09. Goroutines – Concorrência e Paralelismo
- Golang – 10. Goroutines – Canais
- Golang – 11. Pacotes e Documentação no Go
Índice
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].

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:
- Concurrency is not Parallelism – onde apresenta uma visão bem simples mas clara de concorrência e paralelismo;
- artigo de Tony Hoare de 1978 – apresenta o modelo de concorrência que inspirou a implementação de concorrência no Go.
- Concorrência, Paralelismo, Processos, Threads, programação síncrona e assíncrona – texto em português que traz uma discussão bem ampla entrando em threads, sistemas multitarefas, e outros.
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:
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
, linha 14, uma operação de 1 milissegundos, linha 15, e depois um saque de value
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
acrescido é removido da conta 1 milissegundo depois, em situações convencionais.value
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:
…
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:
…
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.
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:
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:
(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 On
ce, 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:
- [Golang] Goroutine – Let’s see how to use thread in Golang by Goroutine – o que me fez olhar com mais atenção o pacote
sync
; - Just Call Your Code Only Once !! blog do Ferdina Kusumah basicamente o mesmo exemplo que empreguei na discussão sobre
sync.Mutex
.
Deixe uma resposta