Aula 41 – Tutorial Golang – Mutexes

Aula 41 – Tutorial Golang – Mutexes

Tutorial Golang

Tutorial Golang

Página principal do blog

Todas as aulas desse curso

Aula 40                        Aula 42

Redes Sociais:

facebook

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

PIX Nubank

PIX Nubank

 

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.

Continue estudando, praticando e explorando novas fronteiras com essa linguagem incrível.

Até a próxima!

Página principal do blog

Todas as aulas desse curso

Aula 40                        Aula 42

Meus links de afiliados:

Hostinger

Digital Ocean

One.com

Obrigado e bons estudos. 😉

About The Author
-

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>