Channels com buffer

1. Introdução aos Channels com Buffer

Channels com buffer em Golang são canais que possuem uma capacidade interna de armazenamento, permitindo que dados sejam enviados sem que haja um receptor imediato. Diferentemente dos channels sem buffer, onde o envio bloqueia até que outra goroutine receba o dado, os channels com buffer oferecem um espaço temporário para armazenar mensagens.

A sintaxe de criação é simples:

ch := make(chan int, 3) // canal com buffer de capacidade 3

O funcionamento básico segue estas regras:
- O envio bloqueia apenas quando o buffer está completamente cheio
- O recebimento bloqueia apenas quando o buffer está vazio
- Enquanto houver espaço no buffer, o produtor pode continuar enviando sem esperar o consumidor

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan string, 2)

    // Envio não bloqueia pois buffer tem espaço
    ch <- "mensagem 1"
    ch <- "mensagem 2"

    fmt.Println("Ambas mensagens enviadas sem bloqueio")

    // Recebimento não bloqueia pois buffer tem dados
    msg1 := <-ch
    msg2 := <-ch
    fmt.Println(msg1, msg2)
}

2. Comportamento de Bloqueio e Não-Bloqueio

O comportamento de bloqueio em channels com buffer é mais flexível que em channels sem buffer. Vamos explorar os cenários possíveis:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int, 2)

    // Buffer parcialmente preenchido - operações não-bloqueantes
    ch <- 1  // não bloqueia
    ch <- 2  // não bloqueia

    // Buffer cheio - próximo envio bloqueia
    go func() {
        ch <- 3 // bloqueia até que alguém receba
        fmt.Println("Terceiro valor enviado")
    }()

    time.Sleep(100 * time.Millisecond)
    fmt.Println("Consumindo um valor...")
    <-ch // libera espaço para o terceiro envio

    time.Sleep(100 * time.Millisecond)
}

Quando o buffer está vazio, o recebimento bloqueia até que um novo dado seja enviado:

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int, 3)

    go func() {
        time.Sleep(1 * time.Second)
        ch <- 42
    }()

    fmt.Println("Aguardando recebimento...")
    valor := <-ch // bloqueia por 1 segundo
    fmt.Println("Recebido:", valor)
}

3. Capacidade e Desempenho

Escolher o tamanho adequado do buffer é crucial para o desempenho da aplicação. Não existe uma fórmula mágica, mas algumas diretrizes ajudam:

package main

import (
    "fmt"
    "time"
)

func processarComBuffer(tamanhoBuffer int) time.Duration {
    inicio := time.Now()
    ch := make(chan int, tamanhoBuffer)

    go func() {
        for i := 0; i < 100000; i++ {
            ch <- i
        }
        close(ch)
    }()

    for range ch {
        // processa
    }

    return time.Since(inicio)
}

func main() {
    fmt.Println("Buffer 1:", processarComBuffer(1))
    fmt.Println("Buffer 10:", processarComBuffer(10))
    fmt.Println("Buffer 100:", processarComBuffer(100))
    fmt.Println("Buffer 1000:", processarComBuffer(1000))
}

Trade-offs importantes:
- Buffer pequeno: menor latência individual, mas mais bloqueios e trocas de contexto
- Buffer grande: maior throughput em picos, mas maior uso de memória e menor sincronismo
- Buffer muito grande: pode mascarar problemas de desempenho e aumentar o consumo de memória

4. Padrões de Uso com Buffer

Produtor-Consumidor com Buffer

O padrão mais comum, suavizando picos de produção:

package main

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

func produtor(ch chan<- int, wg *sync.WaitGroup) {
    defer wg.Done()
    for i := 0; i < 10; i++ {
        valor := rand.Intn(100)
        ch <- valor
        fmt.Printf("Produziu: %d\n", valor)
        time.Sleep(time.Duration(rand.Intn(100)) * time.Millisecond)
    }
}

func consumidor(ch <-chan int, wg *sync.WaitGroup) {
    defer wg.Done()
    for valor := range ch {
        fmt.Printf("Consumiu: %d\n", valor)
        time.Sleep(200 * time.Millisecond) // consumidor mais lento
    }
}

func main() {
    ch := make(chan int, 5) // buffer suaviza produção mais rápida
    var wg sync.WaitGroup

    wg.Add(2)
    go produtor(ch, &wg)
    go consumidor(ch, &wg)

    wg.Wait()
    close(ch)
}

Uso como Semáforo para Limitar Concorrência

package main

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

func main() {
    semaforo := make(chan struct{}, 3) // máximo 3 goroutines simultâneas
    var wg sync.WaitGroup

    for i := 0; i < 10; i++ {
        wg.Add(1)
        go func(id int) {
            defer wg.Done()

            semaforo <- struct{}{} // adquire permissão
            defer func() { <-semaforo }() // libera permissão

            fmt.Printf("Goroutine %d executando\n", id)
            time.Sleep(1 * time.Second)
        }(i)
    }

    wg.Wait()
}

5. Fechamento e Range em Channels com Buffer

O fechamento de channels com buffer segue as mesmas regras dos channels sem buffer, mas com uma diferença importante: dados ainda presentes no buffer podem ser lidos após o fechamento.

package main

import (
    "fmt"
)

func main() {
    ch := make(chan int, 3)

    ch <- 1
    ch <- 2
    ch <- 3
    close(ch) // fecha o canal, mas dados ainda estão no buffer

    // Iteração segura com for range
    for valor := range ch {
        fmt.Println("Valor do buffer:", valor)
    }

    // Verificação de estado com segundo valor booleano
    ch2 := make(chan string, 2)
    ch2 <- "dado"
    close(ch2)

    valor, ok := <-ch2
    fmt.Printf("Valor: %s, Channel aberto: %v\n", valor, ok)

    valor, ok = <-ch2
    fmt.Printf("Valor: %s, Channel aberto: %v\n", valor, ok)
}

6. Armadilhas e Boas Práticas

Deadlock com Buffer

package main

func main() {
    ch := make(chan int, 2)
    ch <- 1
    ch <- 2
    ch <- 3 // deadlock! buffer cheio e ninguém recebe
}

Vazamento de Goroutines

package main

import (
    "fmt"
    "time"
)

func main() {
    ch := make(chan int, 2)

    go func() {
        for i := 0; i < 5; i++ {
            ch <- i
        }
        // Se não fechar o canal, consumidor pode ficar esperando
        close(ch)
    }()

    for valor := range ch {
        fmt.Println(valor)
        if valor == 2 {
            break // consumidor saiu, mas produtor continua?
        }
    }

    time.Sleep(100 * time.Millisecond) // produtor pode estar bloqueado
}

Uso Excessivo de Buffer

// Evite: buffer muito grande que esconde problemas de coordenação
ch := make(chan int, 1000000)

// Prefira: buffer moderado com monitoramento
ch := make(chan int, 100)

7. Comparação com Channels Sem Buffer

Quando usar buffer (comunicação assíncrona):

// Produtor mais rápido que consumidor
ch := make(chan Task, 100) // buffer suaviza picos

Quando evitar buffer (sincronização estrita):

// Sincronização precisa entre goroutines
ch := make(chan struct{}) // sem buffer, garantia de sincronismo

Exemplo prático de migração:

Antes (sem buffer):

func processar(dados []int) []int {
    ch := make(chan int)
    resultado := make([]int, 0, len(dados))

    go func() {
        for _, d := range dados {
            ch <- d * 2
        }
        close(ch)
    }()

    for v := range ch {
        resultado = append(resultado, v)
    }
    return resultado
}

Depois (com buffer):

func processarComBuffer(dados []int) []int {
    ch := make(chan int, len(dados))
    resultado := make([]int, 0, len(dados))

    go func() {
        for _, d := range dados {
            ch <- d * 2 // não bloqueia se buffer tiver espaço
        }
        close(ch)
    }()

    for v := range ch {
        resultado = append(resultado, v)
    }
    return resultado
}

Channels com buffer são ferramentas poderosas quando usadas corretamente. Eles oferecem flexibilidade na comunicação entre goroutines, mas exigem cuidado para evitar deadlocks e vazamentos. A escolha do tamanho do buffer deve ser baseada em medições reais de desempenho e nos requisitos específicos da aplicação.

Referências