18/100 Dias de Golang - Goroutines, WaitGroup, Race condition e Mutex
Table of Contents
#
Goroutines
Vamos continuar a nossa jornada com as goroutines. Quero fazer mais um exemplo aqui.
package main
import (
"fmt"
"time"
)
func task(name string, n int) {
for i := 0; i < n; i++ {
fmt.Printf("Task: %s, i:%d/%d\n", name, i, n)
time.Sleep(500 * time.Millisecond)
}
}
func main() {
go task("1", 5)
go task("2", 7)
time.Sleep(10 * time.Second)
}
Veja a saída desse programa:
Task: 2, i:0/7
Task: 1, i:0/5
Task: 1, i:1/5
Task: 2, i:1/7
Task: 2, i:2/7
Task: 1, i:2/5
Task: 1, i:3/5
Task: 2, i:3/7
Task: 2, i:4/7
Task: 1, i:4/5
Task: 2, i:5/7
Task: 2, i:6/7
Talvez esse exemplo seja mais elucidativo do que o anterior. Nele podemos ver claramente as duas goroutines rodando. O mais legal disso tudo foi a simplicidade de criar uma goroutine. No python, por exemplo, você teria que usar o threading
ou multiprocessing
para fazer isso. Perceba que o time.Sleep(10 * time.Second)
é só um recursso técnico não convencional gambiarra. Uma das formas de resolver isso é um WaitGroup.
#
WaitGroup
No Golang os WaitGroups são utilizados para sincronizar as goroutines. De forma bem resumida, eles são contadores, quando o contador chega a zero, o programa continua. Eles possuem três métodos principais:
Add(int)
: Adiciona um número de goroutines ao contador.Done()
: Decrementa o contador.Wait()
: Bloqueia até que o contador chegue a zero.
Vamos ver o mesmo exemplo anterior, mas agora com o WaitGroup:
package main
import (
"fmt"
"sync"
"time"
)
func task(name string, n int, wgTask *sync.WaitGroup) {
for i := 0; i < n; i++ {
fmt.Printf("Task: %s, i:%d/%d\n", name, i, n)
time.Sleep(500 * time.Millisecond)
}
wgTask.Done()
}
func main() {
var wg sync.WaitGroup
wg.Add(2)
go task("1", 5, &wg)
go task("2", 7, &wg)
wg.Wait()
}
A saída do programa é a mesma, mas agora não precisamos mais do time.Sleep(10 * time.Second)
. O programa só vai continuar quando as duas goroutines terminarem. Veja que definimos o WaitGroup
como var wg sync.WaitGroup
, e depois usamos o wg.Add(2)
para adicionar duas goroutines o contador a 2. Depois, no final da função task
, chamamos o wg.Done()
para decrementar o contador. Por último, chamamos o wg.Wait()
para esperar as goroutines terminarem. Outra alteração que fizemos foi passar o WaitGroup
como parâmetro para a função task
. Isso é necessário porque o WaitGroup
é um tipo de referência, e precisamos passar ele por referência para que as goroutines possam acessá-lo. Quando vamos passar o WaitGroup
como parâmetro, precisamos usar o &
para passar o endereço da variável.
#
Race Condition
Um race condition é quando N threads, N processos ou N qualquer coisas tentam acessar um recurso. Pense que duas threads estão tentando acessar a variável ao mesmo tempo, os valores podem ser sobrescritos e alterar o comportamento da thread. Isso até é uma questão de segurança nos software, veja essa bug encontrado ao resgatar os gift cards no sistema do Starbucks, com ele os atacantes podiam duplicar o valor dos gift cards.
Veja esse programa em Go que vai exibir uma race condition.
package main
import (
"fmt"
"time"
)
var counter int
func increment() {
for i := 0; i < 1000; i++ {
counter++
}
}
func main() {
for i := 0; i < 10; i++ {
go increment()
}
time.Sleep(2 * time.Second)
fmt.Println("Valor final do contador:", counter)
}
O programa é bem simples, temo uma variável global counter
e a função increment
, essa função faz um for de 0 até 1000 e incrementa 1 no counter
. Na função main executamos a função increment
10 vezes e exibimos o resultado. Em teoria, o valor final do counter deveria ser 10000, inclusive, se você retirar o go
antes da função increment
verá o resultado 10000.
Veja a saída de várias execuções:
victorstein in ~/Cursos/go-api-teste took 2,2s
➜ go run main.go
Valor final do contador: 9345
victorstein in ~/Cursos/go-api-teste took 2,2s
➜ go run main.go
Valor final do contador: 7286
victorstein in ~/Cursos/go-api-teste took 2,2s
➜ go run main.go
Valor final do contador: 7246
victorstein in ~/Cursos/go-api-teste took 2,2s
➜ go run main.go
Valor final do contador: 7191
victorstein in ~/Cursos/go-api-teste took 2,2s
➜ go run main.go
Valor final do contador: 7967
victorstein in ~/Cursos/go-api-teste took 3,3s
O compilador do go nos oferece uma ferramenta muito legal para detectar race condition. Basta rodar o programa com o flag -race
go run -race main.go
Veja a saída:
==================
WARNING: DATA RACE
Read at 0x0000005ff610 by goroutine 8:
main.increment()
/home/victorstein/Cursos/go-api-teste/main.go:12 +0x2c
Previous write at 0x0000005ff610 by goroutine 7:
main.increment()
/home/victorstein/Cursos/go-api-teste/main.go:12 +0x44
Goroutine 8 (running) created at:
main.main()
/home/victorstein/Cursos/go-api-teste/main.go:18 +0x32
Goroutine 7 (finished) created at:
main.main()
/home/victorstein/Cursos/go-api-teste/main.go:18 +0x32
==================
Problemas de race condition são muito difíceis de serem debugados e encontrados, e o compilador do go pode nos ajudar nessa tarefa.
#
Mutex
Para resolver esse problema, entra em cena o Mutex - mutual exclusion. Ele nada mais é que um semáforo binário, antes de fazermos uma operação em um recurso compartilhado, colocamos o semáforo na cor vermelha e quando liberamos o uso do recurso colocamos ele na luz verde.
package main
import (
"fmt"
"sync"
"time"
)
var counter int
func increment(mutex *sync.Mutex) {
for i := 0; i < 1000; i++ {
mutex.Lock()
counter++
mutex.Unlock()
}
}
func main() {
var m sync.Mutex
for i := 0; i < 10; i++ {
go increment(&m)
}
time.Sleep(2 * time.Second)
fmt.Println("Valor final do contador:", counter)
}
O que adicionamos aqui foi a criação da variável var m sync.Mutex
e passamos para a função increment
o endereço do mutex. E dentro da função fazer o uso do mutex.Lock()
para trava o recurso e depois o mutex.Unlock()
para destravar. Dessa forma o valor final do contador será o 10000
#
Atomic
Outra forma que o go nos possibilita de resolver esse problema é o pacote atomic
, que realizará a operação de forma atômica.
package main
import (
"fmt"
"sync/atomic"
"time"
)
var counter int32
func increment() {
for i := 0; i < 1000; i++ {
atomic.AddInt32(&counter, 1)
}
}
func main() {
for i := 0; i < 10; i++ {
go increment()
}
time.Sleep(2 * time.Second)
fmt.Println("Valor final do contador:", counter)
}
Ele simplifica a implementação, as únicas alterações no código foram a alteração do tipo da variável para int32
e no for da função increment
usamos atomic.AddInt32(&counter, 1)
, passando o endereço do counter e o valor que queremos adicionar.