paint-brush
Compreendendo o sync.Cond in Go: um guia para iniciantespor@ivanlemeshev
739 leituras
739 leituras

Compreendendo o sync.Cond in Go: um guia para iniciantes

por Ivan Lemeshev9m2024/04/28
Read on Terminal Reader

Muito longo; Para ler

Concluindo, sync.Cond é um tipo útil na linguagem de programação Go que permite a sincronização e coordenação entre goroutines com base em condições específicas. Ele fornece uma maneira de criar e gerenciar variáveis de condição. Possui métodos para aguardar, sinalizar e transmitir condições. Usando `sync.Cond`, você pode escrever programas simultâneos mais controlados e sincronizados em Go. É importante observar que sync.Cond é apenas uma das primitivas de sincronização fornecidas pela biblioteca padrão Go e seu uso depende dos requisitos específicos do seu programa simultâneo. Em alguns casos, outras primitivas de sincronização como canais ou sync.WaitGroup podem ser mais adequadas.
featured image - Compreendendo o sync.Cond in Go: um guia para iniciantes
Ivan Lemeshev HackerNoon profile picture
0-item

Introdução

Quero discutir o tipo de sync.Cond e os casos de uso e quando usá-lo.

O que é sync.Cond ?

Na linguagem de programação Go, sync.Cond é um tipo definido no pacote sync que representa uma variável de condição. Variáveis de condição são primitivas de sincronização usadas para coordenar goroutines, permitindo-lhes esperar que uma condição específica se torne verdadeira antes de prosseguir.


O tipo sync.Cond fornece uma maneira de criar e gerenciar variáveis de condição. Possui três métodos principais:


  1. Wait() : Este método faz com que a goroutine chamadora espere até que outra goroutine sinalize a variável de condição. Quando a goroutine chama Wait() , ela libera o bloqueio associado e suspende a execução até que outra goroutine chame Signal() ou Broadcast() na mesma variável sync.Cond .


  2. Signal() : Este método ativa uma goroutine aguardando a variável de condição. Se vários goroutines estiverem esperando, apenas um deles será despertado. A escolha de qual goroutine será despertada é arbitrária e não garantida.


  3. Broadcast() : Este método ativa todas as goroutines que aguardam a variável de condição. Quando Broadcast() é chamado, todas as goroutines em espera são despertadas e podem prosseguir.


Observe que sync.Cond requer um sync.Mutex associado para sincronizar o acesso à variável de condição.


Ao usar sync.Cond , você pode coordenar a execução de goroutines com base em condições específicas, permitindo uma programação simultânea mais controlada e sincronizada em Go.

Casos de uso comuns

sync.Cond é comumente usado em cenários onde as goroutines precisam coordenar e se comunicar entre si com base em condições específicas. Vamos considerar casos de uso comuns para sync.Cond .

Sincronização de Goroutine

sync.Cond pode ser usado para sincronizar a execução de vários goroutines. Por exemplo, você pode ter várias goroutines que devem aguardar a satisfação de uma condição específica antes de prosseguir. As goroutines em espera podem chamar cond.Wait() , e a goroutine de sinalização pode chamar cond.Signal() ou cond.Broadcast() para ativar as goroutines em espera quando a condição for atendida.


 package main import ( "fmt" "sync" "time" ) func main() { var wg sync.WaitGroup var mu sync.Mutex cond := sync.NewCond(&mu) wg.Add(2) go func() { fmt.Println("Goroutine 1 is started") defer wg.Done() cond.L.Lock() defer cond.L.Unlock() fmt.Println("Goroutine 1 is waiting for condition") cond.Wait() fmt.Println("Goroutine 1 met the condition") fmt.Println("Goroutine 1 is done") }() go func() { fmt.Println("Goroutine 2 is started") defer wg.Done() time.Sleep(5 * time.Second) // Simulating some work cond.L.Lock() defer cond.L.Unlock() fmt.Println("Goroutine 2 is signaling condition") cond.Signal() fmt.Println("Goroutine 2 completed signaling") fmt.Println("Goroutine 2 is done") }() wg.Wait() }


Neste exemplo, temos duas goroutines. A primeira goroutine espera por uma condição usando cond.Wait() , enquanto a segunda goroutine sinaliza a condição usando cond.Signal() .


Quando o programa é executado, a primeira goroutine adquire o bloqueio e então chama cond.Wait() . Como a condição ainda não foi atendida, a primeira goroutine libera o bloqueio e suspende sua execução.


Enquanto isso, a segunda goroutine dorme por cinco segundos, simulando algum trabalho. Ele adquire o bloqueio e depois chama cond.Signal() . Ele ativa a goroutine em espera, que então adquire o bloqueio e executa.


O uso de sync.Cond garante que a primeira goroutine espere até que a segunda goroutine sinalize a condição, permitindo a sincronização e coordenação entre as duas goroutines.

Problema Produtor-Consumidor

sync.Cond pode ser útil na solução do problema produtor-consumidor , um problema clássico de sincronização que envolve dois tipos de processos, produtores e consumidores, que compartilham um buffer ou fila comum de tamanho fixo. As goroutines produtoras podem usar cond.Signal() ou cond.Broadcast() para notificar as goroutines consumidoras quando novos dados estiverem disponíveis para consumo.


 package main import ( "fmt" "sync" "time" ) const MaxMessageChannelSize = 5 func main() { var wg sync.WaitGroup var mu sync.Mutex cond := sync.NewCond(&mu) messageChannel := NewMessageChannel(MaxMessageChannelSize) producer := NewProducer(cond, messageChannel) consumer := NewConsumer(cond, messageChannel) wg.Add(2) go func() { defer wg.Done() for i := range 10 { producer.Produce(fmt.Sprintf("Message %d", i)) } }() go func() { defer wg.Done() for range 10 { consumer.Consume() } }() wg.Wait() } type MessageChannel struct { maxBufferSize int buffer []string } func NewMessageChannel(size int) *MessageChannel { return &MessageChannel{ maxBufferSize: size, buffer: make([]string, 0, size), } } func (mc *MessageChannel) IsEmpty() bool { return len(mc.buffer) == 0 } func (mc *MessageChannel) IsFull() bool { return len(mc.buffer) == mc.maxBufferSize } func (mc *MessageChannel) Add(message string) { mc.buffer = append(mc.buffer, message) } func (mc *MessageChannel) Get() string { message := mc.buffer[0] mc.buffer = mc.buffer[1:] return message } type Producer struct { cond *sync.Cond messageChannel *MessageChannel } func NewProducer(cond *sync.Cond, messageChannel *MessageChannel) *Producer { return &Producer{ cond: cond, messageChannel: messageChannel, } } func (p *Producer) Produce(message string) { time.Sleep(500 * time.Millisecond) // Simulating some work p.cond.L.Lock() defer p.cond.L.Unlock() for p.messageChannel.IsFull() { fmt.Println("Producer is waiting because the message channel is full") p.cond.Wait() } p.messageChannel.Add(message) fmt.Println("Producer produced the message:", message) p.cond.Signal() } type Consumer struct { id int cond *sync.Cond messageChannel *MessageChannel } func NewConsumer(cond *sync.Cond, messageChannel *MessageChannel) *Consumer { return &Consumer{ cond: cond, messageChannel: messageChannel, } } func (c *Consumer) Consume() { time.Sleep(1 * time.Second) // Simulating some work c.cond.L.Lock() defer c.cond.L.Unlock() for c.messageChannel.IsEmpty() { fmt.Println("Consumer is waiting because the message channel is empty") c.cond.Wait() } message := c.messageChannel.Get() fmt.Println("Consumer consumed the message:", message) c.cond.Signal() }


Neste exemplo, temos uma goroutine produtora que produz mensagens e as adiciona ao canal de mensagens e uma goroutine consumidora que consome mensagens. O canal de mensagens possui tamanho máximo definido por MaxMessageChannelSize .


A goroutine produtora adiciona mensagens ao canal de mensagens e usa cond.Signal() para notificar a goroutine consumidora quando novos dados estão disponíveis. Se o canal de mensagens estiver cheio, a goroutine produtora espera usando cond.Wait() até que o consumidor consuma alguns dados e libere espaço no canal de mensagens.


Da mesma forma, a goroutine consumidora consome mensagens do canal de mensagens e usa cond.Signal() para notificar a goroutine produtora quando espaço fica disponível no canal de mensagens. Se estiver vazio, a goroutine do consumidor espera usando cond.Wait() até que o produtor produza alguns dados e os adicione ao canal de mensagens.


Aqui, sync.Cond permite a coordenação e sincronização entre as goroutines produtoras e consumidoras. Garante que o consumidor espere quando o canal de mensagens estiver vazio e o produtor espere quando estiver cheio, resolvendo assim o problema produtor-consumidor.

Sincronização de recursos

Suponha que várias goroutines precisem de acesso exclusivo a um recurso compartilhado. sync.Cond pode ser usado para coordenar o acesso. Por exemplo, um conjunto de goroutines de trabalho pode precisar esperar até que um determinado número de recursos fique disponível antes de poder iniciar o processamento. As goroutines podem aguardar a variável de condição usando cond.Wait() e notificar sobre a liberação de recursos usando cond.Signal() ou cond.Broadcast() .


 package main import ( "fmt" "sync" "time" ) const MaxResources = 3 func main() { var wg sync.WaitGroup var mu sync.Mutex cond := sync.NewCond(&mu) resourceProvider := NewResourceProvider(cond, MaxResources) wg.Add(10) for i := range 10 { go func(workerID int) { defer wg.Done() worker := NewWorker(workerID, cond, resourceProvider) worker.Run() }(i) } wg.Wait() } type ResourceProvider struct { maxResources int availableResources int cond *sync.Cond } func NewResourceProvider(cond *sync.Cond, maxResources int) *ResourceProvider { return &ResourceProvider{ cond: cond, availableResources: maxResources, } } func (rp *ResourceProvider) AvailableResources() int { return rp.availableResources } func (rp *ResourceProvider) AcquireResoirce() { rp.availableResources-- } func (rp *ResourceProvider) ReleaseResource() { rp.availableResources++ } type Worker struct { id int cond *sync.Cond rp *ResourceProvider } func NewWorker(workerID int, cond *sync.Cond, rp *ResourceProvider) *Worker { return &Worker{ id: workerID, cond: cond, rp: rp, } } func (w *Worker) Run() { w.cond.L.Lock() for w.rp.AvailableResources() == 0 { fmt.Printf("Worker %d is waiting for resources\n", w.id) w.cond.Wait() } w.rp.AcquireResoirce() fmt.Printf("Worker %d acquired resource. Remaining resources: %d\n", w.id, w.rp.AvailableResources()) w.cond.L.Unlock() time.Sleep(1 * time.Second) // Simulating work w.cond.L.Lock() defer w.cond.L.Unlock() w.rp.ReleaseResource() fmt.Printf("Worker %d released resource. Remaining resources: %d\n", w.id, w.rp.AvailableResources()) w.cond.Signal() }


Neste exemplo, temos várias goroutines de trabalho que precisam de acesso exclusivo a recursos limitados. As goroutines dos trabalhadores adquirem e liberam recursos usando cond.Signal() para coordenar com outros trabalhadores. Se nenhum recurso estiver disponível, as goroutines de trabalho aguardam using cond.Wait() até que a outra goroutine libere o recurso.


Neste exemplo, sync.Cond permite a sincronização e a coordenação entre as goroutines de trabalho, garantindo que as goroutines de trabalho esperem quando não houver recursos disponíveis, sincronizando efetivamente o acesso aos recursos.

Notificação de Evento

sync.Cond pode ser usado para notificar goroutines sobre eventos ou alterações específicas no sistema. Por exemplo, você pode ter goroutines aguardando um evento específico. Quando o evento acontece, a goroutine de sinalização pode usar cond.Signal() ou cond.Broadcast() para ativar as goroutines em espera e permitir que elas lidem com o evento.


 package main import ( "fmt" "sync" "time" ) const maxWorkersCount = 10 func main() { var counter int32 var wg sync.WaitGroup var mu sync.Mutex cond := sync.NewCond(&mu) wg.Add(maxWorkersCount) for i := range maxWorkersCount { go func(workerID int) { defer wg.Done() fmt.Printf("Worker %d performing work\n", workerID) time.Sleep(1 * time.Second) // Simulate work cond.L.Lock() defer cond.L.Unlock() counter++ if counter == maxWorkersCount { fmt.Println("All workers have reached the barrier") cond.Broadcast() } else { fmt.Printf("Worker %d is waiting at the barrier\n", workerID) cond.Wait() } fmt.Printf("Worker %d passed the barrier\n", workerID) }(i) } wg.Wait() }


Aqui, temos várias goroutines de trabalho que realizam trabalho e sincronizam em um ponto de barreira. As goroutines dos trabalhadores incrementam um contador e então esperam na barreira ou sinalizam a barreira usando cond.Wait() e cond.Broadcast() com base na contagem de trabalhadores que alcançam a barreira.


Cada goroutine trabalhador executa algum trabalho e então adquire o bloqueio para incrementar a variável do contador. Se o trabalhador atual for o último a alcançar a barreira, ele transmite a condição da barreira usando cond.Broadcast() para acordar todos os trabalhadores em espera. Caso contrário, espera na barreira usando cond.Wait() para ser notificado pelo último trabalhador.


A sincronização da barreira garante que todas as goroutines dos trabalhadores alcancem a barreira antes que qualquer uma delas prossiga além dela. Pode ser útil em cenários que exigem a sincronização da execução de várias goroutines em um ponto específico do fluxo de trabalho.


Observe que a barreira é implementada usando um contador simples neste exemplo. No entanto, em cenários mais complexos, poderá ser necessário considerar mecanismos ou condições de sincronização adicionais para garantir a sincronização correta e evitar condições de corrida.

Conclusão

Concluindo, sync.Cond é um tipo útil na linguagem de programação Go que permite a sincronização e coordenação entre goroutines com base em condições específicas. Ele fornece uma maneira de criar e gerenciar variáveis de condição. Possui métodos para aguardar, sinalizar e transmitir condições. Usando `sync.Cond`, você pode escrever programas simultâneos mais controlados e sincronizados em Go.


É importante observar que sync.Cond é apenas uma das primitivas de sincronização fornecidas pela biblioteca padrão Go e seu uso depende dos requisitos específicos do seu programa simultâneo. Em alguns casos, outras primitivas de sincronização como canais ou sync.WaitGroup podem ser mais adequadas.