Go Concurrency Starter Pack
Visão Geral
Breve Introdução
A linguagem Go possui muitas ferramentas para gerenciamento de concorrência. Neste artigo, apresentaremos algumas delas e alguns truques.
Goroutines?
Goroutine é um novo modelo de concorrência suportado pela linguagem Go. Geralmente, para executar várias tarefas simultaneamente, um programa recebe OS threads do sistema operacional e executa tarefas em paralelo conforme o número de núcleos. Para concorrência em unidades menores, são criadas green threads no userspace, permitindo que várias green threads executem tarefas dentro de uma única OS thread. No entanto, no caso das goroutines, essas green threads foram tornadas ainda menores e mais eficientes. As goroutines usam menos memória do que as threads e podem ser criadas e substituídas mais rapidamente do que as threads.
Para usar goroutines, basta utilizar a palavra-chave go. Isso permite que o código síncrono seja executado de forma assíncrona de maneira intuitiva durante o processo de escrita do programa.
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 ch := make(chan struct{})
10 go func() {
11 defer close(ch) // Fecha o canal quando a goroutine termina.
12 time.Sleep(1 * time.Second) // Espera por 1 segundo.
13 fmt.Println("Hello, World!") // Imprime "Hello, World!".
14 }()
15
16 fmt.Println("Waiting for goroutine...") // Imprime "Waiting for goroutine...".
17 for range ch {} // Aguarda o fechamento do canal.
18}
Este código simplesmente transforma um código síncrono que espera 1 segundo e depois imprime Hello, World! em um fluxo assíncrono. Embora este exemplo seja simples, se um código um pouco mais complexo for alterado de síncrono para assíncrono, a legibilidade, visibilidade e compreensibilidade do código serão aprimoradas em comparação com abordagens como async/await ou promises.
No entanto, em muitos casos, se não for compreendido o fluxo de simplesmente chamar código síncrono de forma assíncrona e fluxos como fork & join (semelhante a um fluxo de dividir e conquistar), podem ser produzidos códigos de goroutine inadequados. Este artigo apresentará alguns métodos e técnicas para lidar com tais situações.
Gerenciamento de Concorrência
context
A primeira técnica de gerenciamento a ser mencionada, context, pode ser surpreendente. No entanto, na linguagem Go, context desempenha um papel excelente no gerenciamento de toda a árvore de tarefas, indo além de uma simples funcionalidade de cancelamento. Para aqueles que não estão familiarizados, farei uma breve explicação deste pacote.
1package main
2
3func main() {
4 ctx, cancel := context.WithCancel(context.Background()) // Cria um context com função de cancelamento.
5 defer cancel() // Garante que o cancelamento seja chamado.
6
7 go func() {
8 <-ctx.Done() // Aguarda o sinal de cancelamento do context.
9 fmt.Println("Context is done!") // Imprime quando o context é cancelado.
10 }()
11
12 time.Sleep(1 * time.Second) // Espera por 1 segundo.
13
14 cancel() // Cancela o context.
15
16 time.Sleep(1 * time.Second) // Espera por 1 segundo.
17}
O código acima usa context para imprimir Context is done! após 1 segundo. context pode verificar o status de cancelamento através do método Done() e oferece vários métodos de cancelamento, como WithCancel, WithTimeout, WithDeadline e WithValue.
Vamos criar um exemplo simples. Suponha que você esteja escrevendo um código que usa o padrão aggregator para buscar user, post e comment para obter alguns dados. Se todas as solicitações precisarem ser concluídas em 2 segundos, você pode escrever da seguinte forma:
1package main
2
3func main() {
4 ctx, cancel := context.WithTimeout(context.Background(), 2 * time.Second) // Cria um context com timeout de 2 segundos.
5 defer cancel() // Garante que o cancelamento seja chamado.
6
7 ch := make(chan struct{}) // Cria um canal para sinalizar a conclusão.
8 go func() {
9 defer close(ch) // Fecha o canal quando a goroutine termina.
10 user := getUser(ctx) // Obtém o usuário.
11 post := getPost(ctx) // Obtém o post.
12 comment := getComment(ctx) // Obtém o comentário.
13
14 fmt.Println(user, post, comment) // Imprime os dados.
15 }()
16
17 select {
18 case <-ctx.Done(): // Caso o context seja cancelado (timeout).
19 fmt.Println("Timeout!") // Imprime "Timeout!".
20 case <-ch: // Caso os dados sejam buscados.
21 fmt.Println("All data is fetched!") // Imprime "All data is fetched!".
22 }
23}
O código acima imprime Timeout! se todos os dados não forem buscados em 2 segundos, e imprime All data is fetched! se todos os dados forem buscados. Dessa forma, usando context, você pode gerenciar facilmente o cancelamento e o timeout mesmo em códigos onde várias goroutines estão em execução.
Várias funções e métodos relacionados a context podem ser encontrados em godoc context. Esperamos que você possa aprender o básico e utilizá-los confortavelmente.
channel
unbuffered channel
channel é uma ferramenta para comunicação entre goroutines. Um channel pode ser criado com make(chan T). Aqui, T é o tipo de dado que o channel transmitirá. Dados podem ser enviados e recebidos através do channel usando <-, e o channel pode ser fechado com close.
1package main
2
3func main() {
4 ch := make(chan int) // Cria um canal de inteiros.
5 go func() {
6 ch <- 1 // Envia 1 para o canal.
7 ch <- 2 // Envia 2 para o canal.
8 close(ch) // Fecha o canal.
9 }()
10
11 for i := range ch { // Itera sobre os valores recebidos do canal.
12 fmt.Println(i) // Imprime o valor.
13 }
14}
O código acima imprime 1 e 2 usando um channel. Este código mostra apenas o envio e recebimento de valores em um channel. No entanto, o channel oferece mais funcionalidades do que isso. Primeiro, vamos examinar o buffered channel e o unbuffered channel. Antes de começar, o exemplo acima é um unbuffered channel, onde o ato de enviar dados para o canal e o ato de receber dados devem ocorrer simultaneamente. Se essas ações não ocorrerem simultaneamente, pode ocorrer um deadlock.
buffered channel
E se o código acima não fosse apenas uma saída simples, mas dois processos executando tarefas pesadas? Se o segundo processo travar por um longo período enquanto lê e processa, o primeiro processo também parará durante esse tempo. Podemos usar um buffered channel para evitar essa situação.
1package main
2
3func main() {
4 ch := make(chan int, 2) // Cria um canal de inteiros com buffer de tamanho 2.
5 go func() {
6 ch <- 1 // Envia 1 para o canal.
7 ch <- 2 // Envia 2 para o canal.
8 close(ch) // Fecha o canal.
9 }()
10
11 for i := range ch { // Itera sobre os valores recebidos do canal.
12 fmt.Println(i) // Imprime o valor.
13 }
14}
O código acima imprime 1 e 2 usando um buffered channel. Este código usa um buffered channel para que o ato de enviar dados para o channel e o ato de receber dados não precisem ocorrer simultaneamente. Ao adicionar um buffer a um canal dessa forma, é criada uma margem de manobra equivalente ao seu comprimento, o que pode evitar atrasos nas tarefas causados pelo impacto de tarefas de menor prioridade.
select
Ao lidar com vários canais, a sintaxe select pode ser usada para implementar facilmente uma estrutura fan-in.
1package main
2
3import (
4 "fmt"
5 "time"
6)
7
8func main() {
9 ch1 := make(chan int, 10) // Cria o canal ch1 com buffer.
10 ch2 := make(chan int, 10) // Cria o canal ch2 com buffer.
11 ch3 := make(chan int, 10) // Cria o canal ch3 com buffer.
12
13 go func() {
14 for {
15 ch1 <- 1 // Envia 1 para ch1.
16 time.Sleep(1 * time.Second) // Espera 1 segundo.
17 }
18 }()
19 go func() {
20 for {
21 ch2 <- 2 // Envia 2 para ch2.
22 time.Sleep(2 * time.Second) // Espera 2 segundos.
23 }
24 }()
25 go func() {
26 for {
27 ch3 <- 3 // Envia 3 para ch3.
28 time.Sleep(3 * time.Second) // Espera 3 segundos.
29 }
30 }()
31
32 for i := 0; i < 3; i++ {
33 select {
34 case v := <-ch1: // Tenta receber de ch1.
35 fmt.Println(v) // Imprime o valor.
36 case v := <-ch2: // Tenta receber de ch2.
37 fmt.Println(v) // Imprime o valor.
38 case v := <-ch3: // Tenta receber de ch3.
39 fmt.Println(v) // Imprime o valor.
40 }
41 }
42}
O código acima cria 3 canais que transmitem periodicamente 1, 2 e 3, e usa select para receber e imprimir valores dos canais. Dessa forma, usando select, você pode receber dados de vários canais simultaneamente e processá-los assim que os valores são recebidos dos canais.
for range
Um channel pode receber dados facilmente usando for range. Quando for range é usado em um canal, ele opera sempre que dados são adicionados ao canal, e o loop termina quando o canal é fechado.
1package main
2
3func main() {
4 ch := make(chan int) // Cria um canal de inteiros.
5 go func() {
6 ch <- 1 // Envia 1 para o canal.
7 ch <- 2 // Envia 2 para o canal.
8 close(ch) // Fecha o canal.
9 }()
10
11 for i := range ch { // Itera sobre os valores recebidos do canal.
12 fmt.Println(i) // Imprime o valor.
13 }
14}
O código acima imprime 1 e 2 usando um channel. Este código usa for range para receber e imprimir dados sempre que dados são adicionados ao canal. E o loop termina quando o canal é fechado.
Como escrito algumas vezes acima, esta sintaxe também pode ser usada para um simples mecanismo de sincronização.
1package main
2
3func main() {
4 ch := make(chan struct{}) // Cria um canal de struct vazio.
5 go func() {
6 defer close(ch) // Fecha o canal quando a goroutine termina.
7 time.Sleep(1 * time.Second) // Espera por 1 segundo.
8 fmt.Println("Hello, World!") // Imprime "Hello, World!".
9 }()
10
11 fmt.Println("Waiting for goroutine...") // Imprime "Waiting for goroutine...".
12 for range ch {} // Aguarda o fechamento do canal.
13}
O código acima imprime Hello, World! após esperar 1 segundo. Este código usa um channel para converter um código síncrono em código assíncrono. Dessa forma, usando um channel, você pode facilmente converter código síncrono em código assíncrono e definir pontos de join.
etc
- Enviar ou receber dados de um
nil channelpode levar a um loop infinito, resultando em um deadlock. - Enviar dados para um canal após ele ter sido fechado causará um panic.
- Mesmo que um canal não seja explicitamente fechado, o GC o fechará durante a coleta de lixo.
mutex
spinlock
spinlock é um método de sincronização que tenta adquirir um bloqueio repetidamente em um loop. Na linguagem Go, você pode implementar facilmente um spinlock usando ponteiros.
1package spinlock
2
3import (
4 "runtime"
5 "sync/atomic"
6)
7
8type SpinLock struct {
9 lock uintptr
10}
11
12func (s *SpinLock) Lock() {
13 for !atomic.CompareAndSwapUintptr(&s.lock, 0, 1) { // Tenta adquirir o bloqueio.
14 runtime.Gosched() // Cede o processador para outras goroutines.
15 }
16}
17
18func (s *SpinLock) Unlock() {
19 atomic.StoreUintptr(&s.lock, 0) // Libera o bloqueio.
20}
21
22func NewSpinLock() *SpinLock {
23 return &SpinLock{} // Retorna uma nova instância de SpinLock.
24}
O código acima implementa o pacote spinlock. Este código usa o pacote sync/atomic para implementar SpinLock. O método Lock tenta adquirir o bloqueio usando atomic.CompareAndSwapUintptr, e o método Unlock libera o bloqueio usando atomic.StoreUintptr. Este método tenta adquirir o bloqueio sem descanso, o que significa que ele continuará usando a CPU até que o bloqueio seja obtido, podendo levar a um loop infinito. Portanto, spinlock é recomendado para sincronização simples ou para uso em períodos curtos.
sync.Mutex
mutex é uma ferramenta para sincronização entre goroutines. O mutex fornecido pelo pacote sync oferece métodos como Lock, Unlock, RLock e RUnlock. Um mutex pode ser criado com sync.Mutex, e um sync.RWMutex pode ser usado para bloqueios de leitura/escrita.
1package main
2
3import (
4 "sync"
5)
6
7func main() {
8 var mu sync.Mutex // Declara um Mutex.
9 var count int // Declara uma variável inteira.
10
11 go func() {
12 mu.Lock() // Adquire o bloqueio.
13 count++ // Incrementa count.
14 mu.Unlock() // Libera o bloqueio.
15 }()
16
17 mu.Lock() // Adquire o bloqueio.
18 count++ // Incrementa count.
19 mu.Unlock() // Libera o bloqueio.
20
21 println(count) // Imprime o valor de count.
22}
No código acima, duas goroutines acessam a mesma variável count quase simultaneamente. Ao usar um mutex para tornar o código que acessa a variável count uma seção crítica, o acesso simultâneo à variável count pode ser evitado. Então, este código imprimirá 2 consistentemente, independentemente de quantas vezes seja executado.
sync.RWMutex
sync.RWMutex é um mutex que pode ser usado para distinguir entre bloqueios de leitura e escrita. Os métodos RLock e RUnlock podem ser usados para adquirir e liberar bloqueios de leitura.
1package cmap
2
3import (
4 "sync"
5)
6
7type ConcurrentMap[K comparable, V any] struct {
8 sync.RWMutex // Mutex de leitura-escrita embutido.
9 data map[K]V // Mapa para armazenar os dados.
10}
11
12func (m *ConcurrentMap[K, V]) Get(key K) (V, bool) {
13 m.RLock() // Adquire um bloqueio de leitura.
14 defer m.RUnlock() // Libera o bloqueio de leitura ao sair.
15
16 value, ok := m.data[key] // Acessa os dados.
17 return value, ok
18}
19
20func (m *ConcurrentMap[K, V]) Set(key K, value V) {
21 m.Lock() // Adquire um bloqueio de escrita.
22 defer m.Unlock() // Libera o bloqueio de escrita ao sair.
23
24 m.data[key] = value // Modifica os dados.
25}
O código acima implementa ConcurrentMap usando sync.RWMutex. Neste código, o método Get adquire um bloqueio de leitura e o método Set adquire um bloqueio de escrita, permitindo acesso e modificação seguros ao mapa data. A razão para a necessidade de um bloqueio de leitura é que, em casos de muitas operações de leitura simples, várias goroutines podem executar operações de leitura simultaneamente, adquirindo apenas um bloqueio de leitura em vez de um bloqueio de escrita. Isso pode melhorar o desempenho quando não há necessidade de um bloqueio de escrita porque o estado não está sendo alterado.
fakelock
fakelock é um truque simples que implementa sync.Locker. Esta estrutura fornece os mesmos métodos que sync.Mutex, mas não executa nenhuma operação real.
1package fakelock
2
3type FakeLock struct{} // Declara uma estrutura vazia.
4
5func (f *FakeLock) Lock() {} // Método Lock que não faz nada.
6
7func (f *FakeLock) Unlock() {} // Método Unlock que não faz nada.
O código acima implementa o pacote fakelock. Este pacote implementa sync.Locker fornecendo os métodos Lock e Unlock, mas na verdade não faz nada. A razão pela qual tal código é necessário será explicada se houver uma oportunidade.
waitgroup
sync.WaitGroup
sync.WaitGroup é uma ferramenta que aguarda a conclusão de todas as goroutines. Ele fornece os métodos Add, Done e Wait. O método Add adiciona o número de goroutines, Done sinaliza que o trabalho de uma goroutine foi concluído e Wait aguarda até que o trabalho de todas as goroutines tenha terminado.
1package main
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8func main() {
9 wg := sync.WaitGroup{} // Cria um WaitGroup.
10 c := atomic.Int64{} // Cria um atomic.Int64.
11
12 for i := 0; i < 100 ; i++ {
13 wg.Add(1) // Adiciona 1 ao contador do WaitGroup.
14 go func() {
15 defer wg.Done() // Decrementa o contador do WaitGroup quando a goroutine termina.
16 c.Add(1) // Incrementa c atomicamente.
17 }()
18 }
19
20 wg.Wait() // Aguarda todas as goroutines terminarem.
21 println(c.Load()) // Imprime o valor final de c.
22}
O código acima usa sync.WaitGroup para que 100 goroutines adicionem valores à variável c simultaneamente. Neste código, sync.WaitGroup é usado para aguardar até que todas as goroutines terminem e, em seguida, imprime o valor adicionado à variável c. Embora usar apenas canais seja suficiente para operações de fork & join de algumas tarefas, usar sync.WaitGroup é uma boa opção para operações de fork & join de um grande número de tarefas.
with slice
Quando usado com slices, waitgroup pode ser uma excelente ferramenta para gerenciar operações concorrentes sem bloqueios.
1package main
2
3import (
4 "fmt"
5 "sync"
6 "rand" // Pacote rand para números aleatórios.
7)
8
9func main() {
10 var wg sync.WaitGroup // Declara um WaitGroup.
11 arr := [10]int{} // Declara um array de 10 inteiros.
12
13 for i := 0; i < 10; i++ {
14 wg.Add(1) // Adiciona 1 ao contador do WaitGroup.
15 go func(id int) {
16 defer wg.Done() // Decrementa o contador do WaitGroup quando a goroutine termina.
17
18 arr[id] = rand.Intn(100) // Atribui um número aleatório ao índice id.
19 }(i) // Passa o valor de i como argumento para a goroutine.
20 }
21
22 wg.Wait() // Aguarda todas as goroutines terminarem.
23 fmt.Println("Done") // Imprime "Done".
24
25 for i, v := range arr { // Itera sobre o array.
26 fmt.Printf("arr[%d] = %d\n", i, v) // Imprime o índice e o valor.
27 }
28}
O código acima usa apenas waitgroup para que cada goroutine gere simultaneamente 10 números inteiros aleatórios e os armazene em seus índices atribuídos. Neste código, waitgroup é usado para aguardar até que todas as goroutines terminem e, em seguida, imprime Done. Dessa forma, usando waitgroup, várias goroutines podem executar tarefas simultaneamente, armazenar dados sem bloqueio até que todas as goroutines terminem e realizar pós-processamento em lote após a conclusão da tarefa.
golang.org/x/sync/errgroup.ErrGroup
errgroup é um pacote que estende sync.WaitGroup. Ao contrário de sync.WaitGroup, errgroup cancela todas as goroutines e retorna um erro se qualquer uma das goroutines encontrar um erro.
1package main
2
3import (
4 "context"
5 "fmt"
6 "golang.org/x/sync/errgroup"
7)
8
9func main() {
10 g, ctx := errgroup.WithContext(context.Background()) // Cria um errgroup com context.
11 _ = ctx // Ignora o contexto (neste exemplo).
12
13 for i := 0; i < 10; i++ {
14 i := i // Captura a variável de iteração.
15 g.Go(func() error {
16 if i == 5 {
17 return fmt.Errorf("error") // Retorna um erro se i for 5.
18 }
19 return nil // Retorna nil (sem erro) caso contrário.
20 })
21 }
22
23 if err := g.Wait(); err != nil { // Aguarda a conclusão de todas as goroutines e verifica erros.
24 fmt.Println(err) // Imprime o erro, se houver.
25 }
26}
O código acima usa errgroup para criar 10 goroutines e gera um erro na 5ª goroutine. Eu intencionalmente demonstrei o caso em que um erro ocorre na quinta goroutine. No entanto, na prática, você pode usar errgroup para criar goroutines e realizar vários pós-processamentos para casos em que erros ocorrem em cada goroutine.
once
Uma ferramenta para executar código que deve ser executado apenas uma vez. O código relacionado pode ser executado através dos seguintes construtores.
1func OnceFunc(f func()) func()
2func OnceValue[T any](f func() T) func() T
3func OnceValues[T1, T2 any](f func() (T1, T2)) func() (T1, T2)
OnceFunc
OnceFunc simplesmente garante que a função seja executada exatamente uma vez em toda a sua vida útil.
1package main
2
3import "sync"
4
5func main() {
6 once := sync.OnceFunc(func() { // Cria uma função que será executada apenas uma vez.
7 println("Hello, World!") // Imprime "Hello, World!".
8 })
9
10 once() // Chama a função.
11 once() // Chama a função novamente.
12 once() // Chama a função novamente.
13 once() // Chama a função novamente.
14 once() // Chama a função novamente.
15}
O código acima usa sync.OnceFunc para imprimir Hello, World!. Neste código, sync.OnceFunc é usado para criar a função once, e mesmo que a função once seja chamada várias vezes, Hello, World! será impresso apenas uma vez.
OnceValue
OnceValue não apenas garante que a função seja executada exatamente uma vez, mas também armazena o valor de retorno da função e o retorna em chamadas subsequentes.
1package main
2
3import "sync"
4
5func main() {
6 c := 0 // Declara e inicializa a variável c.
7 once := sync.OnceValue(func() int { // Cria uma função que será executada apenas uma vez e retornará um valor.
8 c += 1 // Incrementa c.
9 return c // Retorna o valor de c.
10 })
11
12 println(once()) // Chama a função e imprime o valor retornado.
13 println(once()) // Chama a função novamente.
14 println(once()) // Chama a função novamente.
15 println(once()) // Chama a função novamente.
16 println(once()) // Chama a função novamente.
17}
O código acima usa sync.OnceValue para incrementar a variável c em 1. Neste código, sync.OnceValue é usado para criar a função once, e mesmo que a função once seja chamada várias vezes, a variável c será incrementada apenas uma vez, retornando 1.
OnceValues
OnceValues funciona da mesma forma que OnceValue, mas pode retornar vários valores.
1package main
2
3import "sync"
4
5func main() {
6 c := 0 // Declara e inicializa a variável c.
7 once := sync.OnceValues(func() (int, int) { // Cria uma função que será executada apenas uma vez e retornará dois valores.
8 c += 1 // Incrementa c.
9 return c, c // Retorna o valor de c duas vezes.
10 })
11
12 a, b := once() // Chama a função e atribui os valores retornados a a e b.
13 println(a, b) // Imprime a e b.
14 a, b = once() // Chama a função novamente.
15 println(a, b) // Imprime a e b.
16 a, b = once() // Chama a função novamente.
17 println(a, b) // Imprime a e b.
18 a, b = once() // Chama a função novamente.
19 println(a, b) // Imprime a e b.
20 a, b = once() // Chama a função novamente.
21 println(a, b) // Imprime a e b.
22}
O código acima usa sync.OnceValues para incrementar a variável c em 1. Neste código, sync.OnceValues é usado para criar a função once, e mesmo que a função once seja chamada várias vezes, a variável c será incrementada apenas uma vez, retornando 1.
atomic
O pacote atomic fornece operações atômicas. O pacote atomic oferece métodos como Add, CompareAndSwap, Load, Store e Swap, mas recentemente é recomendado o uso de tipos como Int64, Uint64 e Pointer.
1package main
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8func main() {
9 wg := sync.WaitGroup{} // Cria um WaitGroup.
10 c := atomic.Int64{} // Cria um atomic.Int64.
11
12 for i := 0; i < 100 ; i++ {
13 wg.Add(1) // Adiciona 1 ao contador do WaitGroup.
14 go func() {
15 defer wg.Done() // Decrementa o contador do WaitGroup quando a goroutine termina.
16 c.Add(1) // Incrementa c atomicamente.
17 }()
18 }
19
20 wg.Wait() // Aguarda todas as goroutines terminarem.
21 println(c.Load()) // Imprime o valor final de c.
22}
Este é o exemplo usado anteriormente. É um código que incrementa atomicamente a variável c usando o tipo atomic.Int64. Os métodos Add e Load podem ser usados para incrementar atomicamente uma variável e lê-la. Além disso, o método Store pode ser usado para armazenar um valor, o método Swap para trocar um valor e o método CompareAndSwap para comparar um valor e trocá-lo se for adequado.
cond
sync.Cond
O pacote cond fornece variáveis de condição. O pacote cond pode ser criado com sync.Cond e oferece os métodos Wait, Signal e Broadcast.
1package main
2
3import (
4 "sync"
5)
6
7func main() {
8 c := sync.NewCond(&sync.Mutex{}) // Cria uma nova variável de condição associada a um Mutex.
9 ready := false // Variável booleana para indicar se está pronto.
10
11 go func() {
12 c.L.Lock() // Adquire o bloqueio do Mutex.
13 ready = true // Define ready como true.
14 c.Signal() // Sinaliza uma goroutine esperando.
15 c.L.Unlock() // Libera o bloqueio do Mutex.
16 }()
17
18 c.L.Lock() // Adquire o bloqueio do Mutex.
19 for !ready { // Loop enquanto ready for false.
20 c.Wait() // Aguarda até que seja sinalizado.
21 }
22 c.L.Unlock() // Libera o bloqueio do Mutex.
23
24 println("Ready!") // Imprime "Ready!".
25}
O código acima usa sync.Cond para aguardar até que a variável ready se torne true. Neste código, sync.Cond é usado para aguardar até que a variável ready se torne true e, em seguida, imprime Ready!. Dessa forma, usando sync.Cond, várias goroutines podem aguardar simultaneamente até que uma condição específica seja satisfeita.
Isso pode ser usado para implementar uma queue simples.
1package queue
2
3import (
4 "sync"
5 "sync/atomic"
6)
7
8type Node[T any] struct {
9 Value T // Valor armazenado no nó.
10 Next *Node[T] // Próximo nó na fila.
11}
12
13type Queue[T any] struct {
14 sync.Mutex // Mutex para proteger o acesso à fila.
15 Cond *sync.Cond // Variável de condição para sinalizar mudanças na fila.
16 Head *Node[T] // Cabeça da fila.
17 Tail *Node[T] // Cauda da fila.
18 Len int // Comprimento da fila.
19}
20
21func New[T any]() *Queue[T] {
22 q := &Queue[T]{} // Cria uma nova fila.
23 q.Cond = sync.NewCond(&q.Mutex) // Associa a variável de condição ao Mutex da fila.
24 return q // Retorna a fila.
25}
26
27func (q *Queue[T]) Push(value T) {
28 q.Lock() // Adquire o bloqueio.
29 defer q.Unlock() // Libera o bloqueio ao sair.
30
31 node := &Node[T]{Value: value} // Cria um novo nó.
32 if q.Len == 0 { // Se a fila estiver vazia.
33 q.Head = node // O novo nó é a cabeça.
34 q.Tail = node // O novo nó é a cauda.
35 } else {
36 q.Tail.Next = node // Adiciona o novo nó ao final.
37 q.Tail = node // Atualiza a cauda.
38 }
39 q.Len++ // Incrementa o comprimento da fila.
40 q.Cond.Signal() // Sinaliza uma goroutine esperando.
41}
42
43func (q *Queue[T]) Pop() T {
44 q.Lock() // Adquire o bloqueio.
45 defer q.Unlock() // Libera o bloqueio ao sair.
46
47 for q.Len == 0 { // Enquanto a fila estiver vazia.
48 q.Cond.Wait() // Aguarda até que um item seja adicionado.
49 }
50
51 node := q.Head // Obtém o nó da cabeça.
52 q.Head = q.Head.Next // Move a cabeça para o próximo nó.
53 q.Len-- // Decrementa o comprimento da fila.
54 return node.Value // Retorna o valor do nó.
55}
Utilizando sync.Cond dessa forma, é possível aguardar eficientemente e retomar a operação quando a condição for satisfeita, em vez de usar spin-lock que consome muitos recursos da CPU.
semaphore
golang.org/x/sync/semaphore.Semaphore
O pacote semaphore fornece semáforos. O pacote semaphore pode ser criado com golang.org/x/sync/semaphore.Semaphore e oferece os métodos Acquire, Release e TryAcquire.
1package main
2
3import (
4 "context"
5 "fmt"
6 "golang.org/x/sync/semaphore"
7)
8
9func main() {
10 s := semaphore.NewWeighted(1) // Cria um semáforo ponderado com peso máximo de 1.
11
12 if s.TryAcquire(1) { // Tenta adquirir um peso de 1.
13 fmt.Println("Acquired!") // Imprime "Acquired!" se for adquirido.
14 } else {
15 fmt.Println("Not Acquired!") // Imprime "Not Acquired!" se não for adquirido.
16 }
17
18 s.Release(1) // Libera um peso de 1.
19}
O código acima usa semaphore para criar um semáforo e demonstra como adquirir e liberar o semáforo usando os métodos Acquire e Release. Neste código, mostrei como adquirir e liberar um semáforo usando semaphore.
Conclusão
Os conteúdos básicos devem ser suficientes até aqui. Com base no conteúdo deste artigo, espero que vocês possam compreender e utilizar na prática os métodos de gerenciamento de concorrência usando goroutines. Espero que este artigo tenha sido útil para vocês. Obrigado.