DEV Community

Matheus Mina
Matheus Mina

Posted on

Destrinchando o pacote sync do Go

Na minha opinião, Go fornece um excelente suporte a se trabalhar concorrentemente não só pelas goroutines, mas pelo ecossistema da linguagem. Um grande exemplo disso é o pacote sync, que auxilia na sincronização das rotinas concorrentes. Neste post, vamos aprofundar em tudo que este pacote pode nos oferecer.

Waitgroups

Waitgroups são utilizados para coordenar a execução de diversas rotinas. Ele facilita a criação e a garantia de que todas as sub-rotinas serão finalizadas antes de finalizar a rotina principal. No post sobre waitgroups explico melhor como elas funcionam e o que mudou com a versão 1.25 do Go.

Mutex

Mutex é o mesmo que mutual exclusion locker, ou em português, tranca de exclusão mútua. A sua função é travar o acesso a um recurso enquanto uma operação é executada, evitando que outras rotinas tentem escrever nesse recurso ao mesmo tempo. Por exemplo, qual o retorno da função a seguir?

func count() int {
    counter := 0
    wg := sync.WaitGroup{}

    for i := 0; i < 1000; i++ {
        wg.Go(func() {
            counter++
        })
    }

    wg.Wait()
    return counter
}
Enter fullscreen mode Exit fullscreen mode

Se a resposta foi 1000, existe alguma chance de você ter acertado, mas é bem improvável. Isso acontece, pois como as rotinas são executadas concorrentemente, elas podem tentar escrever no recurso ao mesmo tempo. Para garantir que isso não aconteça, basta adicionar uma mutex e travar o acesso àquele recurso.

func count() int {
    counter := 0
    mu := sync.Mutex{}
    wg := sync.WaitGroup{}

    for i := 0; i < 1000; i++ {
        wg.Go(func() {
            mu.Lock()
            counter++
            mu.Unlock()
        })
    }

    wg.Wait()
    return counter
}
Enter fullscreen mode Exit fullscreen mode

O uso é bem simples, para travar o acesso a um registro você usa a função Lock e, ao finalizar, é só utilizar o Unlock. Só é necessária atenção para não cair em deadlock. Também existe a função TryLock, que valida se existe uma trava ativa ou não, porém seu caso de uso é mais raro.

RW Mutex

O RW Mutex é uma evolução da mutex onde existem travas específicas para escrita e leitura. Essa distinção é bastante útil quando uma ou mais rotinas precisam acessar um recurso só para leitura, mas não querem que o objeto seja modificado durante sua execução. Contudo, é importante citar que a escrita tem mais prioridade que a leitura e, dessa forma, Go evita o starvation.

var numbers []int
var mu sync.RWMutex

func store(x int) {
    mu.Lock()
    numbers = append(numbers, x)
    mu.Unlock()
}

func avg() float64 {
    mu.RLock()
    defer mu.RUnlock()

    size := len(numbers)
    sum := 0
    for _, n := range numbers {
        sum += n
    }

    return float64(sum) / float64(size)
}
Enter fullscreen mode Exit fullscreen mode

No exemplo acima, podemos ter diversas rotinas chamando o avg para pegar a média da lista de inteiros, contudo, se uma rotina decidir inserir mais um valor, todos vão ter que esperar essa escrita finalizar.

Atomic

O atomic é um subpacote do pacote sync que implementa suporte à concorrência em tipos primitivos. Atualmente, ele suporta os seguintes tipos: bool, int32, int64, pointer, uint32, uint64, uintpointer e value. Com ele, podemos simplificar o exemplo utilizado na mutex:

func countWithAtomic() atomic.Int32 {
    var counter atomic.Int32
    wg := sync.WaitGroup{}

  counter.Add(1)
    for i := 0; i < 1000; i++ {
        wg.Go(func() {
            v, ok := counter.Load

        })
    }

    wg.Wait()
    return counter.Load()
}
Enter fullscreen mode Exit fullscreen mode

É necessário notar que as operações básicas, como adição, foram reimplementadas para garantir que as rotinas não concorram pelo recurso.

Map

O Map é como qualquer outro map normal. Ele fornece funções para comparar, trocar, atribuir ou recuperar os valores, com a diferença de ser seguro para a concorrência.

func mapExample() int {
    var m sync.Map
    wg := sync.WaitGroup{}

    for i := 0; i < 1000; i++ {
        wg.Go(func() {
            m.LoadOrStore(i, i*i)
        })
    }

    wg.Wait()

    v, _ := m.Load(0)
    return v.(int)
}
Enter fullscreen mode Exit fullscreen mode

A própria documentação sugere que ele deve ser utilizado em dois casos:

  1. Quando uma chave é escrita somente uma vez, mas lida diversas vezes. Um exemplo é um cache que só cresce.
  2. Quando múltiplas goroutines leem e escrevem grupos distintos de chaves.

Qualquer outro caso é melhor utilizar o map tradicional com mutexes.

Once

O tipo Once garante que algo será executado uma única vez, mesmo que diversas rotinas tentem executar. Um exemplo disso poderia ser a inicialização de recursos, como é demonstrado pela documentação do Go.

func doSomething() int {
    wg := sync.WaitGroup{}
    o := sync.Once{}
    result := 0

    for i := 0; i < 10; i++ {
        wg.Go(func() {
            o.Do(func() {
                result++
            })
        })
    }

    wg.Wait()
    return result
}
Enter fullscreen mode Exit fullscreen mode

É importante se atentar que, caso a função entre em pânico, ela não será reexecutada.

Cond

Como o próprio nome diz, o Cond funciona a partir de uma condicional, ou seja, quando algo acontece, ele libera a execução de uma rotina. Essa execução pode ser liberada uma a uma utilizando a função signal ou ativando todas de uma só vez com o broadcast.

func condExample() {
    mu := sync.Mutex{}
    cond := sync.NewCond(&mu)
    wg := sync.WaitGroup{}
    active := false

    for i := 0; i < 1000; i++ {
        wg.Go(func() {
            cond.L.Lock()
            defer cond.L.Unlock()

            for !active {
                cond.Wait()
            }

            fmt.Println("Active is true, printing: ", i)
        })
    }

    // Activate all goroutines after some time
    time.Sleep(time.Second * 5)
    fmt.Println("Setting Active to true...")
    active = true
    fmt.Println("Wake up one goroutine...")
    cond.Signal()

    time.Sleep(time.Second * 5)
    fmt.Println("Wake up all goroutines...")
    cond.Broadcast()

    wg.Wait()
    fmt.Println("All goroutines finished.")
}
Enter fullscreen mode Exit fullscreen mode

Primeiro, iniciamos um cond com algum Locker, uma interface que implementa as funções Lock e Unlock, no exemplo, utilizamos um mutex. Ao inicializar cada goroutine, é necessário garantir o lock e então a colocamos em estado de espera com o Wait, que retira o lock, permitindo que as novas rotinas sejam iniciadas. Quando uma rotina é liberada com o Signal ou Broadcast, o Wait pega o Lock novamente e libera a execução do código. A documentação do Go recomenda que o Wait aconteça dentro de um laço esperando uma condição, pois o Cond sozinho não consegue dizer se algo aconteceu ou não, mas isso não é estritamente necessário. O fluxo geral então é:

goroutine garante o lockwait libera o lockwait aguarda um sinalwait recebe um sinalwait garante novo lockwait libera a execuçãogoroutine realiza o trabalhogoroutine libera o lock

Pool

O Pool fornece uma maneira de se lidar com objetos de curta duração na memória. Isso ajuda a aliviar a pressão no GC, pois o espaço de memória é sempre reutilizado. A documentação oficial cita como exemplo o pacote fmt, que usa pools como buffers temporários de saída que ajustam seu tamanho conforme a necessidade.

type Message struct {
    Text string
}

var p = sync.Pool{
    New: func() any { return new(Message) },
}

func poolExample() {
    v := p.Get().(*Message)
    defer p.Put(v)

    v.Text = "hello guys"
}
Enter fullscreen mode Exit fullscreen mode

Para inicializar uma Pool precisamos definir a sua função de inicialização. Ao utilizar o Get recuperamos o que está salvo na memória e com o Put escrevemos um novo valor nela. O New só é utilizado se não existir nada alocado na memória.

Conclusão

O pacote sync fornece diversas funcionalidades que são extremamente úteis ao se trabalhar com múltiplas goroutines. É possível controlar a execução com os tipos Cond e Once. Waitgroups garantem que tudo será executado. Mutex, RWMutex e os tipos atômicos evitam a concorrência pelos recursos. Por fim, o Pool alivia o trabalho do GC quando é possível trabalhar com objetos de curta duração na memória. Sem sombra de dúvidas, esse pacote é crucial para quem trabalha com goroutines. Caso você queira entender os detalhes de implementação deste pacote, recomendo a palestra apresentada na Gophercon UK 2025, Deep dive into the sync package, apresentada pelo Jesus Hawthorn. E também tem uma apresentação que fiz na Golang SP sobre o tema. Diga nos comentários se já utilizou este pacote e se de alguma forma ele te ajudou. Se ainda não utilizou, comente o que achou do post.

Links Extras

Top comments (0)