Aula 41 – Tutorial Golang – Mutexes
Aula 41 – Tutorial Golang – Mutexes
Página principal do blog
Todas as aulas desse curso
Aula 40 Aula 42
Redes Sociais:
Meus links de afiliados:
Hostinger
Digital Ocean
One.com
Melhore seu NETWORKING
https://digitalinnovation.one/
Participe de comunidades de desenvolvedores:
Fiquem a vontade para me adicionar ao linkedin.
E também para me seguir no https://github.com/toticavalcanti.
Código final da aula:
https://github.com/toticavalcanti
Canais do Youtube
Toti
Lofi Music Zone Beats
Backing Track / Play-Along
Código Fluente
Putz!
Vocal Techniques and Exercises
PIX para doações
Aula 41 – Tutorial Golang – Mutexes
No aula anterior, vimos como gerenciar o estado de um contador simples usando operações atômicas.
Para estados mais complexos, podemos usar um mutex para acessar dados de forma segura entre várias goroutines.
1. Sincronização de Contadores com Mutex em Go
O Container contém um mapa de contadores, como queremos atualizá-lo concorrentemente a partir de múltiplas goroutines, adicionamos um Mutex para sincronizar o acesso.
type Container struct {
mu sync.Mutex
counters map[string]int
}
Note que mutexes não devem ser copiados, então, se essa struct for passada adiante, isso deve ser feito por ponteiro.
Bloqueia o mutex antes de acessar os contadores e desbloqueia no final da função usando uma instrução defer.
func (c *Container) inc(name string) {
c.mu.Lock()
defer c.mu.Unlock()
c.counters[name]++
}
Veja que o valor zero de um mutex é utilizável como está, portanto, nenhuma inicialização é necessária aqui.
func main() {
c := Container{
counters: map[string]int{"a": 0, "b": 0},
}
var wg sync.WaitGroup
Esta função incrementa um contador chamado no loop.
doIncrement := func(name string, n int) {
for i := 0; i < n; i++ {
c.inc(name)
}
wg.Done()
}
Executa várias goroutines simultaneamente.
Note que todas elas acessam o mesmo Container e duas delas acessam o mesmo contador.
wg.Add(3)
go doIncrement("a", 10000)
go doIncrement("a", 10000)
go doIncrement("b", 10000)
Aguarda o término das goroutines e imprime.
wg.Wait()
fmt.Println(c.counters)
Código Completo
// No exemplo anterior vimos como gerenciar o estado de um contador simples
// usando operações atômicas. Para estados mais complexos, podemos usar um _mutex_
// para acessar dados de forma segura através de múltiplas goroutines.
package main
import (
"fmt"
"sync"
)
// Container armazena um mapa de contadores; como queremos
// atualizá-lo concorrentemente a partir de múltiplas goroutines, nós
// adicionamos um `Mutex` para sincronizar o acesso.
// Note que mutexes não devem ser copiados, então, se essa
// `struct` for passada adiante, isso deve ser feito por
// ponteiro.
type Container struct {
mu sync.Mutex
counters map[string]int
}
func (c *Container) inc(name string) {
// Bloqueie o mutex antes de acessar `counters`; desbloqueie
// no final da função usando uma instrução [defer](defer).
c.mu.Lock()
defer c.mu.Unlock()
c.counters[name]++
}
func main() {
c := Container{
// Observe que o valor zero de um mutex é utilizável como está, então nenhuma
// inicialização é necessária aqui.
counters: map[string]int{"a": 0, "b": 0},
}
var wg sync.WaitGroup
// Esta função incrementa um contador nomeado
// em um loop.
doIncrement := func(name string, n int) {
for i := 0; i < n; i++ {
c.inc(name)
}
wg.Done()
}
// Execute várias goroutines simultaneamente; note
// que todas acessam o mesmo `Container`,
// e duas delas acessam o mesmo contador.
wg.Add(3)
go doIncrement("a", 10000)
go doIncrement("a", 10000)
go doIncrement("b", 10000)
// Aguarde o término das goroutines
wg.Wait()
fmt.Println(c.counters)
}
Ao executar o programa, observa-se que os contadores foram atualizados conforme esperado.
go run mutexes.go
Saída:
map[a:20000 b:10000]
2. Registro de Logs Concorrentes
Em aplicações de múltiplas goroutines, é comum que várias partes da aplicação precisem registrar logs simultaneamente.
Sem sincronização adequada, as mensagens de log podem se sobrepor ou corromper, dificultando a leitura e análise dos logs.
package main
import (
"fmt"
"sync"
"time"
)
// Logger contém um Mutex para sincronizar o acesso ao recurso compartilhado, neste caso, a saída padrão.
type Logger struct {
mu sync.Mutex
}
// Log imprime uma mensagem no console. O acesso ao método é sincronizado usando o Mutex.
func (l *Logger) Log(msg string) {
l.mu.Lock()
defer l.mu.Unlock()
fmt.Println(time.Now().Format("15:04:05"), msg)
}
func main() {
logger := Logger{}
var wg sync.WaitGroup
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
logger.Log(fmt.Sprintf("Goroutine %d is logging", id))
}(i)
}
wg.Wait()
}
Componentes da Estrutura
type Logger struct {
mu sync.Mutex
}
Logger: este é o nome da estrutura que você definiu em Go.
Estruturas em Go são coleções de campos que você pode usar para agrupar dados juntos.
Neste caso, a estrutura Logger é definida para ser usada como um logger.
mu sync.Mutex: dentro da estrutura Logger, há um único campo chamado mu, que é do tipo sync.Mutex.
Descrição Detalhada
sync.Mutex: Mutex é uma abreviação para Mutual Exclusion (Exclusão Mútua).
O sync.Mutex é uma estrutura fornecida pelo pacote sync do Go, que oferece uma maneira simples de sincronizar o acesso a recursos compartilhados entre múltiplas goroutines.
Ao bloquear o mutex antes de acessar um recurso compartilhado e desbloqueá-lo após o acesso, você garante que apenas uma goroutine possa acessar o recurso por vez, evitando condições de corrida.
Uso do Mutex no Logger: No contexto do Logger, o mutex mu é utilizado para sincronizar o acesso à saída padrão.
Isto é importante porque, em um ambiente concorrente, múltiplas goroutines podem tentar escrever para a saída padrão ao mesmo tempo, o que pode resultar em saídas entrelaçadas ou corrompidas.
Ao usar o mu para garantir que apenas uma goroutine possa executar o método Log de uma vez, a integridade da saída logada é preservada.
Assinatura da Função Log
func (l *Logger) Log(msg string)
(l *Logger): Este é o receptor do método.
Ele indica que Log é um método associado à estrutura Logger.
O receptor é nomeado l é um ponteiro para uma instância de Logger, permitindo que o método modifique o estado do objeto Logger.
A utilização de um ponteiro (*Logger) em vez de um valor (Logger) é crucial para métodos que precisam modificar o estado do receptor ou para evitar a cópia do objeto quando o método é chamado.
Log(msg string): Log é o nome do método, e msg string é o parâmetro que o método aceita.
A msg é uma string que contém a mensagem a ser logada.
Quando você chama esse método, você passa a mensagem que deseja logar como argumento.
Corpo da Função Log
l.mu.Lock()
defer l.mu.Unlock()
fmt.Println(time.Now().Format("15:04:05"), msg)
l.mu.Lock(): aqui, o método tenta adquirir um bloqueio no mutex mu que é parte da estrutura Logger.
Isso é feito para garantir que, quando múltiplas goroutines tentam logar mensagens ao mesmo tempo, apenas uma por vez possa executar a seção de código que segue, preservando a ordem das mensagens logadas e evitando condições de corrida.
defer l.mu.Unlock(): Esta linha desbloqueia o mutex mu assim que a função completa sua execução.
O defer garante que Unlock seja chamado automaticamente ao final da execução da função Log, mesmo se a função sair prematuramente devido a um return ou um pânico.
Isso é importante para prevenir deadlocks por esquecer de desbloquear o mutex.
fmt.Println(time.Now().Format(“15:04:05”), msg): Finalmente, a função loga a mensagem.
Ela utiliza fmt.Println para imprimir a mensagem passada ao método, prefixada com um timestamp formatado.
O time.Now().Format(“15:04:05”) obtém a hora atual e a formata em horas, minutos e segundos, seguido pela mensagem que se deseja logar.
3. Sistema de Reserva de Assentos com Mutex
Imagine um cenário onde um teatro disponibiliza online os ingressos para uma nova peça.
Para evitar que dois ou mais usuários reservem o mesmo assento simultaneamente, usaremos um mutex para sincronizar o acesso à estrutura de dados que representa os assentos disponíveis.
package main
import (
"fmt"
"sync"
)
// Theater representa um teatro com um conjunto de assentos e um Mutex para controlar o acesso concorrente.
type Theater struct {
mu sync.Mutex
seats map[int]bool // true se o assento estiver reservado
}
// NewTheater inicializa um novo teatro com n assentos disponíveis.
func NewTheater(n int) *Theater {
seats := make(map[int]bool)
for i := 1; i <= n; i++ {
seats[i] = false // todos os assentos estão inicialmente disponíveis
}
return &Theater{seats: seats}
}
// Reserve tenta reservar um assento. Retorna true se a reserva foi bem-sucedida.
func (t *Theater) Reserve(seatNumber int) bool {
t.mu.Lock()
defer t.mu.Unlock()
if t.seats[seatNumber] {
return false // Assento já reservado
}
t.seats[seatNumber] = true // Reserva o assento
return true
}
func main() {
theater := NewTheater(10) // Um teatro com 10 assentos
var wg sync.WaitGroup
// Simula várias pessoas tentando reservar o mesmo assento ao mesmo tempo
for i := 0; i < 5; i++ {
wg.Add(1)
go func(id, seatNumber int) {
defer wg.Done()
success := theater.Reserve(seatNumber)
if success {
fmt.Printf("Cliente %d reservou o assento %d\n", id, seatNumber)
} else {
fmt.Printf("Cliente %d falhou ao tentar reservar o assento %d (já reservado)\n", id, seatNumber)
}
}(i, 5) // Todos tentando reservar o assento número 5
}
wg.Wait()
}
Explicação:
Componentes da Estrutura
type Theater struct {
mu sync.Mutex
seats map[int]bool // true se o assento estiver reservado
}
A estrutura Theater representa um teatro. Ela contém dois campos:
mu sync.Mutex: mutex utilizado para sincronizar o acesso ao mapa de assentos, prevenindo condições de corrida quando múltiplas goroutines tentam modificar o mapa simultaneamente.
seats map[int]bool: Um mapa representando os assentos disponíveis no teatro.
A chave é um número de assento, e o valor é um booleano que indica se o assento está reservado (true) ou disponível (false).
Assinatura da Função NewTheater
func NewTheater(n int) *Theater
func: palavra-chave que indica a definição de uma função em Go.
NewTheater: nome da função.
A convenção de nomes em Go sugere começar com uma letra maiúscula para funções que serão exportadas (visíveis fora do pacote).
(n int): lista de parâmetros.
Aqui, n é o único parâmetro, um inteiro que representa o número total de assentos no teatro.
*Theater: tipo de retorno da função.
Retorna um ponteiro para uma instância da estrutura Theater.
Corpo da Função NewTheater
seats := make(map[int]bool)
for i := 1; i <= n; i++ {
seats[i] = false // todos os assentos estão inicialmente disponíveis
}
return &Theater{seats: seats}
Inicialização do Mapa seats
seats := make(map[int]bool): cria um mapa vazio onde a chave é um int (número do assento) e o valor é um bool (indicando se o assento está reservado ou não).
O make é uma função built-in que inicializa mapas, slices e channels.
Loop para Inicializar os Assentos:
for i := 1; i <= n; i++: um loop que começa em 1 e vai até n (inclusive), usado aqui para inicializar cada assento como disponível (false).
seats[i] = false: dentro do loop, cada assento (i) é inicialmente definido como false, indicando que está disponível.
Retorno
return &Theater{seats: seats}: retorna uma instância da estrutura Theater, inicializada com o mapa de assentos seats.
&Theater{…}: o operador & é usado para criar um ponteiro para a instância da estrutura Theater, conforme exigido pelo tipo de retorno da função.
Assinatura da Função Reserve
func (t *Theater) Reserve(seatNumber int) bool
(t *Theater): este é o receptor do método, indicando que Reserve é um método que pertence à estrutura Theater.
O receptor t é um ponteiro para uma instância de Theater, permitindo que o método modifique o estado da instância.
Reserve(seatNumber int) bool: reserve é o nome do método.
Ele recebe um argumento seatNumber do tipo int, que especifica o número do assento a ser reservado e retorna um bool indicando se a reserva foi bem-sucedida (true) ou não (false).
Corpo da Função Reserve
t.mu.Lock()
defer t.mu.Unlock()
if t.seats[seatNumber] {
return false // Assento já reservado
}
t.seats[seatNumber] = true // Reserva o assento
return true
t.mu.Lock(): o método inicia adquirindo um bloqueio no mutex mu para garantir acesso exclusivo à seção crítica que modifica o estado dos assentos.
Isso é crucial para evitar condições de corrida em um ambiente concorrente.
defer t.mu.Unlock(): Esta linha garante que o mutex mu seja desbloqueado quando o método termina sua execução, seja por um retorno normal ou por um caminho de saída prematuro.
O defer adia a execução de t.mu.Unlock() até que as operações restantes no método sejam concluídas, garantindo que o bloqueio seja sempre liberado e evitando deadlocks.
if t.seats[seatNumber] { return false }: aqui, o método verifica se o assento especificado por seatNumber já está reservado (true).
Se estiver, o método retorna false, indicando que a tentativa de reserva falhou porque o assento já está ocupado.
t.seats[seatNumber] = true: se o assento estiver disponível (não reservado), o método reserva o assento marcando-o como true no mapa seats.
return true: Após reservar o assento com sucesso, o método retorna true, indicando que a reserva foi bem-sucedida.
O método Reserve encapsula a lógica de reserva de assentos de forma segura em um ambiente concorrente.
Ele usa um mutex para sincronizar o acesso ao mapa de assentos, garantindo que apenas uma goroutine possa modificar o estado dos assentos de cada vez.
Isso evita sobreposições ou inconsistências nas reservas, tornando o sistema de reserva confiável mesmo sob alta concorrência.
Ao retornar um valor booleano, o método Reserve fornece feedback imediato sobre o sucesso ou falha da operação de reserva, permitindo que o chamador tome ações adequadas com base no resultado da reserva.
Este exemplo ilustra um caso de uso real de mutexes para gerenciar recursos compartilhados (assentos) em um ambiente concorrente, como uma aplicação web que vende ingressos para eventos.
É um padrão comum em sistemas de reservas online, garantindo a consistência e evitando condições de corrida.
E assim concluímos nossa exploração sobre o uso de mutexes na programação concorrente com Go.
Ao longo desta aula, vimos como os mutexes são fundamentais para garantir o acesso seguro a dados compartilhados entre múltiplas goroutines.
Com os exemplos apresentados, desde o simples contador até o sistema de reserva de assentos, você teve a oportunidade de ver a aplicabilidade dos mutexes em cenários reais, refletindo desafios comuns enfrentados por desenvolvedores no dia a dia.
Lembre-se, a programação concorrente traz consigo a necessidade de atenção redobrada com relação à sincronização e à prevenção de condições de corrida.
Os mutexes, embora simples, são ferramentas poderosas que, se usadas corretamente, podem garantir a integridade e a confiabilidade dos seus programas.
Encorajo você a experimentar com os conceitos aprendidos, aplicando-os em seus próprios projetos e explorando ainda mais as possibilidades que a concorrência em Go oferece.
A prática é fundamental para solidificar seu entendimento e habilidade em resolver problemas de sincronização de forma eficaz.
Por fim, espero que esta aula tenha sido esclarecedora e que agora você se sinta mais confortável e confiante para trabalhar com mutexes em Go.