congdong007

Penetration Test、Software Developer

0%

Common worker pool patterns in Go

  1. What is a Worker Pool?

    • A worker pool is a concurrency pattern designed to control the number of goroutines executing tasks concurrently. Instead of spawning an unbounded goroutine for every incoming job—which may lead to excessive memory consumption and scheduler overhead—a worker pool pre-spawns a fixed number of workers (goroutines). Tasks are then dispatched to these workers via a shared queue.

    • Benefits:

      • Limits resource usage by capping concurrency.

      • Prevents unbounded goroutine creation.

      • Provides a simple scheduling mechanism for tasks.

  2. Common Worker Pool Implementations in Go

    • (a) Channel-Based Pool

      • This is the most idiomatic and straightforward implementation.

      • A channel acts as a job queue.

      • Workers are goroutines reading from this channel.

      • The main function submits tasks by sending them into the channel.

      • Example:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        jobs := make(chan int, 100)
        results := make(chan int, 100)

        for w := 0; w < 3; w++ {
        go func(id int) {
        for j := range jobs {
        // process job j
        results <- j * 2
        }
        }(w)
        }

        // submit jobs
        for j := 1; j <= 5; j++ {
        jobs <- j
        }
        close(jobs)
      • This approach is best for bounded tasks where the job set is known in advance.

    • (b) Channel + WaitGroup Pool

      • A slightly more structured approach uses a sync.WaitGroup to wait for all workers to complete.

      • Workers continuously read from a job channel.

      • A WaitGroup ensures the main goroutine blocks until all jobs are finished.

      • This pattern is suited for long-running pipelines where tasks keep arriving dynamically.

    • (c) Encapsulated Worker Pool

      • In larger applications, it’s common to encapsulate the pool into a reusable type. For example:

        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14
        15
        16
        17
        18
        19
        20
        21
        type WorkerPool struct {
        tasks chan func()
        wg sync.WaitGroup
        }

        func NewWorkerPool(n int) *WorkerPool {
        pool := &WorkerPool{tasks: make(chan func())}
        pool.wg.Add(n)
        for i := 0; i < n; i++ {
        go func() {
        defer pool.wg.Done()
        for task := range pool.tasks {
        task()
        }
        }()
        }
        return pool
        }

        func (p *WorkerPool) Submit(task func()) { p.tasks <- task }
        func (p *WorkerPool) Shutdown() { close(p.tasks); p.wg.Wait() }
      • This design allows submitting arbitrary functions and cleanly shutting down the pool. It is often used in task scheduling systems or background workers.

    • (d) Using a Library (e.g., ants)

      • For production-grade performance, developers often use libraries such as ants.

      • Provides a high-performance worker pool with memory reuse.

      • Significantly reduces garbage collection (GC) overhead compared to spawning millions of goroutines.

      • Simple API:

        1
        2
        3
        4
        5
        6
        p, _ := ants.NewPool(10)
        defer p.Release()

        p.Submit(func() {
        // task logic
        })
      • This is widely adopted in scenarios with high throughput and large task volumes.

  3. Summary

    • Channel-based pools: idiomatic and simple, good for small-scale concurrency.

    • Channel + WaitGroup: suitable for dynamic task streams.

    • Encapsulated worker pools: reusable and flexible, used in larger systems.

    • Third-party libraries (e.g., ants): optimized for high performance and reduced GC load.