Pular para o conteúdo principal

victorstein.dev

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.