🚀 Aula 42 – Tutorial Golang – Stateful Goroutines

🚀 Aula 42 – Tutorial Golang – Stateful Goroutines

Tutorial Golang

Tutorial Golang

Página principal do blog

Todas as aulas desse curso

Aula 41                        Aula 43

Redes Sociais:

facebook

 

Conecte-se comigo!

LinkedIn: Fique à vontade para me adicionar no LinkedIn.

Ao conectar-se comigo, você terá acesso a atualizações regulares sobre desenvolvimento web, insights profissionais e oportunidades de networking no setor de tecnologia.

GitHub: Siga-me no GitHub para ficar por dentro dos meus projetos mais recentes, colaborar em código aberto ou simplesmente explorar os repositórios que eu contribuo, o que pode ajudar você a aprender mais sobre programação e desenvolvimento de software.

Recursos e Afiliados

Explorando os recursos abaixo, você ajuda a apoiar nosso site.

Somos parceiros afiliados das seguintes plataformas:

  • Hostinger – Hospedagem web acessível e confiável.
  • Digital Ocean – Infraestrutura de nuvem para desenvolvedores.
  • One.com – Soluções simples e poderosas para o seu site.

Código da aula: Github

Educação e Networking

Amplie suas habilidades e sua rede participando de cursos gratuitos e comunidades de desenvolvedores:

Canais do Youtube

Explore nossos canais no YouTube para uma variedade de conteúdos educativos e de entretenimento, cada um com um foco único para enriquecer sua experiência de aprendizado e lazer.

Toti

Toti: Meu canal pessoal, onde posto clips artesanais de músicas que curto tocar, dicas de teoria musical, entre outras coisas.

Lofi Music Zone Beats

Lofi Music Zone Beats: O melhor da música Lofi para estudo, trabalho e relaxamento, criando o ambiente perfeito para sua concentração.

Backing Track / Play-Along

Backing Track / Play-Along: Acompanhe faixas instrumentais para prática musical, ideal para músicos que desejam aprimorar suas habilidades.

Código Fluente

Código Fluente: Aulas gratuitas de programação, devops, IA, entre outras coisas.

Putz!

Putz!: Canal da banda Putz!, uma banda virtual, criada durante a pandemia com mais 3 amigos, Fábio, Tatá e Lula.

Vocal Techniques and Exercises

Vocal Techniques and Exercises: Melhore suas técnicas vocais com exercícios práticos e dicas de especialistas em canto.

PIX para doações

PIX Nubank

PIX Nubank

🚀 Aula 42 – Tutorial Golang – Stateful Goroutines

Stateful Goroutines são um poderoso padrão de design em Go para gerenciar o estado compartilhado em um ambiente concorrente.

Este padrão se alinha com a filosofia do Go de “não comunicar compartilhando memória, em vez disso, compartilhar memória comunicando-se“.

Não comunicar compartilhando memória

Este princípio desencoraja o padrão de design onde várias threads ou goroutines acessam diretamente e modificam um mesmo pedaço de memória (como variáveis globais ou estruturas de dados compartilhadas) para realizar comunicação entre si.

Esse acesso direto necessita de sincronização complexa, geralmente usando mecanismos como mutexes (travas), para evitar problemas de concorrência, como condições de corrida, onde a ordem de execução não determinística das operações pode levar a resultados inesperados ou erros.

Em vez disso, compartilhar memória comunicando-se

Go propõe uma abordagem diferente para a comunicação entre goroutines: em vez de várias goroutines acessarem diretamente o mesmo estado (compartilhamento de memória), elas devem se comunicar enviando e recebendo dados explicitamente através de canais.

Nesse modelo, cada peça de dados é “propriedade” de uma única goroutine.

Se outra goroutine precisa desses dados ou deseja modificar esses dados, ela deve comunicar-se através de canais, pedindo à goroutine “proprietária” dos dados que realize a operação.

Isso elimina a necessidade de sincronização explícita para acesso ao estado compartilhado, pois a comunicação através de canais já é segura por design.

Vejamos um Exemplo

Imagine um programa que mantém um contador.

Ao invés de várias goroutines incrementarem esse contador diretamente (compartilhamento de memória) e precisarem sincronizar o acesso usando mutexes (o que seria “comunicar compartilhando memória”), uma única goroutine poderia gerenciar o contador.

Outras goroutines enviariam mensagens (por exemplo, um valor +1) para essa goroutine gestora através de um canal sempre que precisassem incrementar o contador (isso seria “compartilhar memória comunicando-se”).

A goroutine gestora lê essas mensagens e atualiza o contador de forma segura, pois ela é a única com acesso direto ao contador.

Essa abordagem simplifica o design do programa, tornando-o mais fácil de entender, manter e livre de erros comuns de programação concorrente.

Por Que e Quando Usar

  • Segurança de Concorrência: Quando múltiplas goroutines precisam acessar ou modificar um estado compartilhado, o uso de Stateful Goroutines pode ajudar a evitar erros comuns relacionados à concorrência, como condições de corrida.
  • Simplicidade e Clareza: Este padrão pode simplificar o design de sistemas concorrentes, tornando o código mais fácil de entender e manter. A lógica de manipulação do estado é centralizada, evitando a complexidade de sincronizar o acesso ao estado através de mutexes ou outras primitivas de sincronização.
  • Padrões de Comunicação Flexíveis: Stateful Goroutines permitem que você modele uma variedade de padrões de comunicação entre goroutines, facilitando a implementação de sistemas complexos, como pipelines de processamento, sistemas de mensagens e gerenciadores de recursos.

Quando Não Usar

  • Desempenho em Alto Volume: Embora Stateful Goroutines sejam eficientes para muitos casos de uso, eles podem se tornar um gargalo se o volume de solicitações for extremamente alto, visto que o acesso ao estado é serializado através de uma única goroutine.
  • Sistemas Simples com Pouca Concorrência: Para sistemas simples ou quando a concorrência não é uma preocupação significativa, a introdução de Stateful Goroutines pode ser desnecessária e levar a uma complexidade adicional sem benefícios claros.
  • Necessidade de Acesso Concorrente de Alta Performance: Se o seu sistema requer acesso concorrente de alta performance ao estado compartilhado, estratégias como o uso de várias goroutines com segmentação do estado ou outras técnicas avançadas de concorrência podem ser mais apropriadas.

Exemplo 1: Sistema de Gerenciamento de Estado Concorrente

Este código exemplifica um sistema de gerenciamento de estado concorrente em Go, utilizando o conceito de Stateful Goroutines, isto é, Rotina Go com Estado.

O objetivo é gerenciar o acesso concorrente a um estado compartilhado (neste caso, um mapa) de forma segura, usando canais para sincronização.

Imagine uma central de atendimento onde apenas um atendente (a Stateful Goroutine) tem acesso a um arquivo de documentos (o estado compartilhado).

Cada pessoa que liga (outras goroutines) faz um pedido específico.

Algumas pedem para ler um documento (readOp), outras para adicionar ou alterar informações (writeOp) e algumas para remover um documento (deleteOp).

Cada pedido é feito por meio de um sistema de tickets (canais), garantindo que o atendente atenda a um pedido de cada vez.

Após o atendente processar o pedido, ele envia uma resposta pelo sistema, indicando que a ação foi concluída.

Conceito de Stateful Goroutines

Conceito de Stateful Goroutines


package main

import (
	"fmt"
	"math/rand"
	"sync/atomic"
	"time"
)

type readOp struct {
	key  int
	resp chan int
}

type writeOp struct {
	key  int
	val  int
	resp chan bool
}

type deleteOp struct {
	key  int
	resp chan bool
}

type readAllOp struct {
	resp chan map[int]int
}

func main() {
	var readOps uint64
	var writeOps uint64
	var deleteOps uint64

	reads := make(chan readOp)
	writes := make(chan writeOp)
	deletes := make(chan deleteOp)
	readAlls := make(chan readAllOp)

	// Goroutine que gerencia o estado
	go func() {
		var state = make(map[int]int)
		for {
			select {
			case read := <-reads:
				read.resp <- state[read.key]
			case write := <-writes:
				state[write.key] = write.val
				write.resp <- true
			case delOp := <-deletes: // Renomeei 'delete' para 'delOp'
				_, exists := state[delOp.key]
				if exists {
					delete(state, delOp.key) // Uso correto da função delete
					delOp.resp <- true
				} else {
					delOp.resp <- false
				}
			case readAll := <-readAlls:
				stateCopy := make(map[int]int)
				for k, v := range state {
					stateCopy[k] = v
				}
				readAll.resp <- stateCopy
			}
		}
	}()

	// Simulando operações de leitura, escrita e deleção
	for i := 0; i < 100; i++ {
		go func() {
			read := readOp{
				key:  rand.Intn(5),
				resp: make(chan int)}
			reads <- read
			<-read.resp
			atomic.AddUint64(&readOps, 1)
		}()
	}

	for i := 0; i < 10; i++ {
		go func() {
			write := writeOp{
				key:  rand.Intn(5),
				val:  rand.Intn(100),
				resp: make(chan bool)}
			writes <- write
			<-write.resp
			atomic.AddUint64(&writeOps, 1)
		}()
	}

	for i := 0; i < 5; i++ {
		go func() {
			delete := deleteOp{
				key:  rand.Intn(5),
				resp: make(chan bool)}
			deletes <- delete
			<-delete.resp
			atomic.AddUint64(&deleteOps, 1)
		}()
	}

	time.Sleep(time.Second)

	// Solicitar e imprimir o estado final
	readAll := readAllOp{resp: make(chan map[int]int)}
	readAlls <- readAll
	stateCopy := <-readAll.resp
	fmt.Println("Final state:", stateCopy)

	// Impressão dos contadores de operações
	fmt.Println("Read operations:", atomic.LoadUint64(&readOps))
	fmt.Println("Write operations:", atomic.LoadUint64(&writeOps))
	fmt.Println("Delete operations:", atomic.LoadUint64(&deleteOps))
}

Vamos detalhar o fluxo de execução:

Inicialização e Setup

  • Estruturas: O código define estruturas para representar operações de leitura (readOp), escrita (writeOp), remoção(deleteOp) e leitura total (readAllOp) sobre o estado compartilhado. Cada operação tem um canal resp para enviar a resposta.
  • Variáveis de Contagem: São inicializadas variáveis para contar o número de operações de leitura (readOps), escrita (writeOps) e deleção (deleteOps) realizadas, usando tipos atômicos para garantir a segurança na concorrência.
  • Canais de Operação: São criados canais para cada tipo de operação (reads, writes, deletes, readAll), que serão usados para receber solicitações dessas operações.

Goroutine de Gerenciamento de Estado

  • Uma goroutine é iniciada para gerenciar o estado compartilhado, implementada em um loop infinito que aguarda operações solicitadas através dos canais.
    • Leitura: Responde com o valor atual da chave solicitada.
    • Escrita: Atualiza o valor para a chave especificada.
    • Deleção: Remove a chave especificada do estado, se existir.
    • Leitura Total: Envia uma cópia do estado atual, evitando condições de corrida.

Simulação de Operações

  • O código simula operações concorrentes de leitura, escrita e deleção ao criar múltiplas goroutines que enviam solicitações através dos respectivos canais. Cada goroutine espera por uma resposta antes de incrementar o contador de operações, utilizando operações atômicas para garantir a correta contagem mesmo em um ambiente concorrente.

Espera e Resultados

  • O programa então pausa por um segundo (time.Sleep(time.Second)) para permitir que as goroutines concluam suas operações.
  • Após a pausa, o código solicita e imprime o estado final usando a operação de leitura total (readAllOp). Isso fornece uma visão do estado após as operações de leitura, escrita e deleção.

Saída do Programa

  • Finalmente, o programa imprime os contadores de operações de leitura, escrita e deleção, mostrando quantas operações de cada tipo foram realizadas.

Este código demonstra um padrão poderoso em Go para gerenciar estado compartilhado de forma segura em um ambiente de programação concorrente.

Ele utiliza Stateful Goroutines e canais para sincronizar o acesso ao estado, garantindo que as operações sejam realizadas de forma ordenada e segura, evitando problemas comuns de concorrência como condições de corrida.

Exemplo 2: Sistema de Filas de Mensagens

Outro uso comum para Stateful Goroutines é implementar um sistema de filas de mensagens, onde mensagens podem ser enviadas e consumidas por diferentes partes de um aplicativo.


package main

import (
	"fmt"
	"sync"
	"time"
)

type message struct {
	content string
}

type sendMessage struct {
	msg  message
	resp chan bool
}

type getMessage struct {
	resp chan message
}

func messageQueue() (chan sendMessage, chan getMessage) {
	sendChan := make(chan sendMessage)
	getChan := make(chan getMessage)

	go func() {
		var queue []message
		for {
			if len(queue) > 0 {
				select {
				case msg := <-sendChan:
					queue = append(queue, msg.msg)
					msg.resp <- true
				case get := <-getChan:
					msg := queue[0]
					queue = queue[1:]
					get.resp <- msg
				}
			} else {
				msg := <-sendChan
				queue = append(queue, msg.msg)
				msg.resp <- true
			}
		}
	}()

	return sendChan, getChan
}

func main() {
	send, get := messageQueue()

	var wg sync.WaitGroup

	// Produtor de mensagens
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < 5; i++ {
			respChan := make(chan bool) // create a response channel for each message
			send <- sendMessage{msg: message{content: fmt.Sprintf("Mensagem %d", i)}, resp: respChan}
			ok := <-respChan // wait for the send to be acknowledged
			if !ok {
				fmt.Println("Erro ao enviar mensagem")
				return
			}
			time.Sleep(1 * time.Second)
		}
	}()

	// Consumidor de mensagens
	wg.Add(1)
	go func() {
		defer wg.Done()
		for i := 0; i < 5; i++ {
			respChan := make(chan message) // create a response channel for each get request
			get <- getMessage{resp: respChan}
			msg := <-respChan // wait for the message to be received
			fmt.Println("Recebido:", msg.content)
		}
	}()

	wg.Wait() // wait for both producer and consumer to finish
}

Vamos detalhar o fluxo de execução:

1. Definição das Estruturas de Dados

  • message: Estrutura simples que contém apenas um campo content do tipo string, usado para armazenar o conteúdo da mensagem.
  • sendMessage: Estrutura que inclui uma message e um canal de resposta resp do tipo chan bool. Este canal é utilizado para sinalizar quando a mensagem foi processada (ou seja, adicionada à fila).
  • getMessage: Estrutura que possui um canal de resposta resp do tipo chan message. Este canal é utilizado para receber uma mensagem que foi retirada da fila.

2. Função messageQueue

Esta função inicializa e retorna dois canais: sendChan para enviar mensagens e getChan para receber mensagens. A função também inicia uma goroutine que gerencia a fila de mensagens:

  • Loop Infinito: A goroutine entra em um loop infinito, onde aguarda operações nos canais de envio e recebimento.
    • sendChan: Quando uma mensagem é enviada através deste canal, ela é adicionada à fila (queue), e uma confirmação é enviada de volta pelo canal resp da estrutura sendMessage.
    • getChan: Quando uma solicitação é feita através deste canal, a primeira mensagem da fila é removida e enviada de volta ao solicitante pelo canal resp da estrutura getMessage. Se a fila estiver vazia, a goroutine bloqueia neste ponto até que uma mensagem seja enviada.

3. Função main

A função principal do programa, onde a fila de mensagens é criada e são iniciadas as goroutines para produtores e consumidores de mensagens:

  • Inicialização de Goroutines:
    • Produtor de Mensagens: Uma goroutine é criada para simular um produtor de mensagens. Ela envia um total de 5 mensagens, com intervalos de 1 segundo entre cada envio. Cada mensagem é enviada através do canal sendChan.
    • Consumidor de Mensagens: Outra goroutine é iniciada para simular um consumidor de mensagens. Ela tenta receber um total de 5 mensagens através do canal getChan. Cada mensagem recebida é impressa na saída padrão.
  • Sincronização com WaitGroup: Um sync.WaitGroup é utilizado para garantir que o programa principal (main) aguarde a conclusão de ambas as goroutines antes de terminar. Isso evita que o programa termine prematuramente, o que interromperia as goroutines antes que elas completassem suas tarefas.

4. Conclusão do Processo

Após as goroutines produtora e consumidora completarem suas execuções, o WaitGroup sinaliza que o programa pode terminar, e a função main conclui sua execução.

Este fluxo de execução mostra um exemplo prático e conciso de como goroutines e canais podem ser utilizados para implementar um sistema de filas de mensagens em Go, demonstrando a comunicação e sincronização eficazes entre diferentes partes de um programa.

Fico por aqui.

Continue aperfeiçoando suas habilidades e descobrindo as possibilidades ilimitadas que esta linguagem fascinante oferece.

Até a próxima!

Página principal do blog

Todas as aulas desse curso

Aula 41                        Aula 43

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>