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")
}