Playground , Repo

Why do you need concurrency limiting?

Concurrency limiting in your go application might be necessary to limit overuse of a specific resource. This could be an API rate limit, slow network connection, slow disk I/O operation or limited CPU/RAM.

Starting a huge number of go routines that use a limited resource could cause your application to crash. If you are experiencing crashes or errors on a resource intensive task then I would recommend implementing a concurrency limiter.

How do you implement concurrency limiting?

A buffered channel is the most basic implementation. Write to the channel to acquire a position in the limiter. Read from the channel to open a new position in the limiter. Allow the blocking nature of a channel that is full to stop the execution of individual go routines, then resume execution when the channel is unblocked.

Example Limiter

The following example uses a buffered channel to limit the number of concurrent go routines. The Run() method will start a go routine then pause it if the channel is full. The Wait() method will block until all the go routines have finished executing.

package limiter

import "sync"

type Limiter struct {
	pool chan struct{}
	wg   sync.WaitGroup
}

func New(n int) *Limiter {
	return &Limiter{pool: make(chan struct{}, n), wg: sync.WaitGroup{}}
}

func (l *Limiter) Run(task func()) {
	l.wg.Add(1)
	go func() {
		l.pool <- struct{}{}
		task()
		<-l.pool
		l.wg.Done()
	}()
}

func (l *Limiter) Wait() {
	l.wg.Wait()
}

Example Usage

package main

import (
	"fmt"
	"time"

	"github.com/kfelter/limiter"
)

func main() {
	limiter := limiter.New(10)
	for i := 0; i < 1_000; i++ {
		i := i
		limiter.Run(func() {
			// code to be executed with the limiter
			fmt.Println("hello", i)
		})
	}
	limiter.Wait()

	fmt.Println("done")
}