Introdução ao Go Ants

O ants é um pool de goroutines de alto desempenho que implementa o agendamento e gerenciamento de um grande número de goroutines, permitindo a limitação e reutilização de recursos, alcançando assim uma execução mais eficiente de tarefas ao desenvolver programas concorrentes.

Características

  • Agenda automaticamente um grande número de goroutines e as reutiliza.
  • Limpe regularmente goroutines expiradas para economizar ainda mais recursos.
  • Fornece um grande número de interfaces úteis: submissão de tarefas, obtenção do número de goroutines em execução, ajuste dinâmico do tamanho do pool, liberação do pool e reinicialização do pool.
  • Trata panics de forma graciosa para evitar crashes do programa.
  • A reutilização de recursos economiza consideravelmente o uso de memória. Em cenários de tarefas concorrentes em lote em grande escala, apresenta desempenho superior à concorrência nativa de goroutines.
  • Mecanismo não bloqueante.

Como ants funciona

Fluxograma

ants-flowchart-cn

Imagens Animadas

Instalação

Usando a versão ants v1:

go get -u github.com/panjf2000/ants

Usando a versão ants v2 (habilitar GO111MODULE=on):

go get -u github.com/panjf2000/ants/v2

Uso

Ao escrever um programa concorrente Go que lança um grande número de goroutines, irá inevitavelmente consumir uma grande quantidade de recursos do sistema (memória, CPU). Com o uso do ants, é possível instanciar um pool de goroutines para reutilizar goroutines, economizando recursos e melhorando o desempenho:

package main

import (
	"fmt"
	"sync"
	"sync/atomic"
	"time"

	"github.com/panjf2000/ants/v2"
)

var sum int32

func minhaFuncao(i interface{}) {
	n := i.(int32)
	atomic.AddInt32(&sum, n)
	fmt.Printf("executando com %d\n", n)
}

func minhaFuncaoDemo() {
	time.Sleep(10 * time.Millisecond)
	fmt.Println("Olá Mundo!")
}

func main() {
	defer ants.Release()

	vezesExecutar := 1000

	// Use o pool comum.
	var wg sync.WaitGroup
	sincronizarCalcularSoma := func() {
		minhaFuncaoDemo()
		wg.Done()
	}
	for i := 0; i < vezesExecutar; i++ {
		wg.Add(1)
		_ = ants.Submit(sincronizarCalcularSoma)
	}
	wg.Wait()
	fmt.Printf("goroutines em execução: %d\n", ants.Running())
	fmt.Printf("todas as tarefas concluídas.\n")

	// Use o pool com uma função,
	// defina 10 como a capacidade do pool de goroutines e 1 segundo como duração expirada.
	p, _ := ants.NewPoolWithFunc(10, func(i interface{}) {
		minhaFuncao(i)
		wg.Done()
	})
	defer p.Release()
	// Envie tarefas uma a uma.
	for i := 0; i < vezesExecutar; i++ {
		wg.Add(1)
		_ = p.Invoke(int32(i))
	}
	wg.Wait()
	fmt.Printf("goroutines em execução: %d\n", p.Running())
	fmt.Printf("todas as tarefas concluídas, o resultado é %d\n", sum)
}

Configuração de Pool

// Option representa a função opcional.
type Option func(opts *Options)

// Options contém todas as opções que serão aplicadas ao instanciar uma pool de goroutines.
type Options struct {
	// ExpiryDuration é um período para a gorrotina de limpeza limpar os trabalhadores expirados,
	// a gorrotina de limpeza verifica todos os trabalhadores a cada `ExpiryDuration` e limpa aqueles que não foram
	// usados por mais de `ExpiryDuration`.
	ExpiryDuration time.Duration

	// PreAlloc indica se deve realizar pré-alocação de memória ao inicializar a Pool.
	PreAlloc bool

	// Máximo número de gorrotinas bloqueadas em pool.Submit.
	// 0 (valor padrão) significa que não há limite.
	MaxBlockingTasks int

	// Quando Nonblocking é true, Pool.Submit nunca será bloqueado.
	// ErrPoolOverload será retornado quando Pool.Submit não puder ser feito imediatamente.
	// Quando Nonblocking é true, MaxBlockingTasks é inoperante.
	Nonblocking bool

	// PanicHandler é usado para lidar com panics de cada gorrotina trabalhadora.
	// se nulo, panics serão lançados novamente das gorrotinas trabalhadoras.
	PanicHandler func(interface{})

	// Logger é o logger personalizado para registrar informações, se não estiver configurado,
	// o logger padrão do pacote de log será usado.
	Logger Logger
}

// WithOptions aceita a configuração completa das opções.
func WithOptions(options Options) Option {
	return func(opts *Options) {
		*opts = options
	}
}

// WithExpiryDuration configura o intervalo de tempo para limpar as gorrotinas.
func WithExpiryDuration(expiryDuration time.Duration) Option {
	return func(opts *Options) {
		opts.ExpiryDuration = expiryDuration
	}
}

// WithPreAlloc indica se deve alocar memória para os trabalhadores.
func WithPreAlloc(preAlloc bool) Option {
	return func(opts *Options) {
		opts.PreAlloc = preAlloc
	}
}

// WithMaxBlockingTasks configura o número máximo de gorrotinas que estão bloqueadas quando atinge a capacidade da pool.
func WithMaxBlockingTasks(maxBlockingTasks int) Option {
	return func(opts *Options) {
		opts.MaxBlockingTasks = maxBlockingTasks
	}
}

// WithNonblocking indica que a pool retornará nil quando não houver trabalhadores disponíveis.
func WithNonblocking(nonblocking bool) Option {
	return func(opts *Options) {
		opts.Nonblocking = nonblocking
	}
}

// WithPanicHandler configura o tratador de panic.
func WithPanicHandler(panicHandler func(interface{})) Option {
	return func(opts *Options) {
		opts.PanicHandler = panicHandler
	}
}

// WithLogger configura um logger personalizado.
func WithLogger(logger Logger) Option {
	return func(opts *Options) {
		opts.Logger = logger
	}
}

Ao utilizar várias funções opcionais ao chamar NewPool/NewPoolWithFunc, os valores de cada item de configuração em ants.Options podem ser configurados e, em seguida, usados para personalizar a pool de goroutines.

Pool Personalizada

ants suporta a instanciação da própria Pool do usuário com uma capacidade de pool específica; ao chamar o método NewPool, uma nova Pool com a capacidade especificada pode ser instanciada da seguinte forma:

p, _ := ants.NewPool(10000)

Submissão de Tarefa

As tarefas são submetidas chamando o método ants.Submit(func()):

ants.Submit(func(){})

Ajustando Dinamicamente a Capacidade da Pool de Goroutines

Para ajustar dinamicamente a capacidade da pool de goroutines, você pode usar o método Tune(int):

pool.Tune(1000) // Ajusta sua capacidade para 1000
pool.Tune(100000) // Ajusta sua capacidade para 100000

Este método é seguro para threads.

Pré-alocar memória para a fila de goroutines

O ants permite pré-alocar memória para toda a pool, o que pode melhorar o desempenho da pool de goroutines em cenários específicos. Por exemplo, em um cenário onde uma pool com uma capacidade muito grande é necessária e cada tarefa dentro da goroutine é demorada, pré-alocar memória para a fila de goroutines reduzirá a realocação desnecessária de memória.

// ants pré-alocará toda a capacidade da pool quando você invocar esta função
p, _ := ants.NewPool(100000, ants.WithPreAlloc(true))

Liberar a Pool

pool.Release()

Reiniciar a Pool

// Ao chamar o método Reboot(), você pode reativar uma pool que foi anteriormente destruída e colocá-la de volta em uso.
pool.Reboot()

Sobre a ordem de execução da tarefa

O ants não garante a ordem de execução das tarefas, e a ordem de execução não corresponde necessariamente à ordem de submissão. Isso ocorre porque o ants processa todas as tarefas submetidas de forma concorrente, e as tarefas serão atribuídas aos workers que estão sendo executados simultaneamente, resultando na execução de tarefas em uma ordem concorrente e não determinística.