How to limit the number of simultaneously running goroutines and wait for their completion
Tags: go, goroutine
Published: 2022-11-05 16:17:51

## Table Of Contents
  * [Launching goroutines](#chapter-1)
  * [Starting goroutines with a delay in completing `main()`](#chapter-2)
  * [Waiting for the completion of all goroutines](#chapter-3)
  * [Limiting the number of simultaneously running goroutines](#chapter-4)

Sometimes there is a task to limit the number of simultaneously running goroutines to comply with some limits. For example, by the number of CPU cores or by the memory consumed, etc.

## Launching goroutines <a name="chapter-1"></a>

To begin with, let's see how to run goroutines. Let's run 5 gorutins in a loop, we simulate the work of the goroutine through `time.Sleep()`, we will set the delay as `i * 100ms`. The string `i := i` is needed to localize the value of the loop counter for iteration, without this, each goroutine will get the value 5. Read more [here](https://github.com/golang/go/wiki/CommonMistakes) and [here](https://eli.thegreenplace.net/2019/go-internals-capturing-loop-variables-in-closures/).


```go
package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("START main")
	for i := 0; i < 5; i++ {
		i := i
		go func() {
			delay := time.Millisecond * time.Duration(100*(i+1))
			fmt.Printf("  START #%d (delay %v)\n", i, delay)
			time.Sleep(delay)
			fmt.Printf("  END #%d (delay %v)\n", i, delay)
		}()
	}
	fmt.Println("END main")
}
```

Output:
```
START main
END main

Program exited.
```

[Playground](https://go.dev/play/p/xDte-VcNzcn)

It follows from the output of the program that not a single goroutine was executed. This happened because the execution of the program (function `main()`) ended immediately after the completion of the `for` loop, so all running goroutines were also completed almost immediately after their execution began.

## Starting goroutines with a delay in completing `main()` <a name="chapter-2"></a>

Let's see what happens if some delay is introduced before the program ends, for example 250ms.

```go
package main

import (
	"fmt"
	"time"
)

func main() {
	fmt.Println("START main")
	for i := 0; i < 5; i++ {
		i := i
		go func() {
			delay := time.Millisecond * time.Duration(100*(i+1))
			fmt.Printf("  START #%d (delay %v)\n", i, delay)
			time.Sleep(delay)
			fmt.Printf("  END #%d (delay %v)\n", i, delay)
		}()
	}
  	// Let's add some delay
	time.Sleep(time.Millisecond * 250)
	fmt.Println("END main")
}
```

Output:
```
START main
  START #4 (delay 500ms)
  START #0 (delay 100ms)
  START #1 (delay 200ms)
  START #2 (delay 300ms)
  START #3 (delay 400ms)
  END #0 (delay 100ms)
  END #1 (delay 200ms)
END main

Program exited.
```

[Playground](https://go.dev/play/p/LCnSLSCg8f1)

As you can see, after adding a delay to the `main()` function, two goroutines (with duration of 100ms and 200ms) managed to be executed. If the delay is increased to 500ms or more, then all the goroutines will have time to be executed.

## Waiting for the completion of all goroutines <a name="chapter-3"></a>

In real life, it is rarely possible to determine the execution time of goroutines, therefore, to wait for the completion of all goroutines, you can use `WaitGroup` from the `sync` module. From [documentation](https://pkg.go.dev/sync#WaitGroup ) module:
> A WaitGroup waits for a collection of goroutines to finish. The main goroutine calls Add to set the number of goroutines to wait for. Then each of the goroutines runs and calls Done when finished. At the same time, Wait can be used to block until all goroutines have finished.

In our example, we will call `Add(1)` before each start of the goroutine, and `Done()` when the goroutine is completed via `defer`.

```go
package main

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

func main() {
	fmt.Println("START main")
	var wg sync.WaitGroup
	for i := 0; i < 5; i++ {
		i := i
		wg.Add(1)
		go func() {
			defer wg.Done()
			delay := time.Millisecond * time.Duration(100*(i+1))
			fmt.Printf("  START #%d (delay %v)\n", i, delay)
			time.Sleep(delay)
			fmt.Printf("  END #%d (delay %v)\n", i, delay)
		}()
	}
	wg.Wait()
	fmt.Println("END main")
}
```

Output:
```
START main
  START #4 (delay 500ms)
  START #2 (delay 300ms)
  START #3 (delay 400ms)
  START #1 (delay 200ms)
  START #0 (delay 100ms)
  END #0 (delay 100ms)
  END #1 (delay 200ms)
  END #2 (delay 300ms)
  END #3 (delay 400ms)
  END #4 (delay 500ms)
END main

Program exited.
```

[Playground](https://go.dev/play/p/U7DNztI4ETH)

We see that all the goroutines have time to complete, but the problem of simultaneous launch of all goroutines has not yet been solved.

## Limiting the number of simultaneously running goroutines <a name="chapter-4"></a>

To limit the number of simultaneously running goroutines, a buffered channel can be used. The feature of the buffered channel is that after the buffer is filled, the next write to the channel is blocked until at least one value is read from the channel.

The constant `grMax` contains the number of simultaneously executed goroutines. After the channel buffer is full, execution will be blocked on the command `ch <- 1` and will continue after reading from it with the command `<-ch`. It is better to place the command to read from the channel in `defer` so that when errors occur in the gorutin, the entire program does not hang.

```go
package main

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

const grMax = 3

func main() {
	fmt.Println("START main")
	var wg sync.WaitGroup
	ch := make(chan int, grMax)
	for i := 0; i < 5; i++ {
		i := i
		wg.Add(1)
		ch <- 1
		go func() {
			defer func() { wg.Done(); <-ch }()
			delay := time.Millisecond * time.Duration(100*(i+1))
			fmt.Printf("  START #%d (delay %v)\n", i, delay)
			time.Sleep(delay)
			fmt.Printf("  END #%d (delay %v)\n", i, delay)
		}()
	}
	wg.Wait()
	fmt.Println("END main")
}
```

Output:
```
START main
  START #2 (delay 300ms)
  START #0 (delay 100ms)
  START #1 (delay 200ms)
  END #0 (delay 100ms)
  START #3 (delay 400ms)
  END #1 (delay 200ms)
  START #4 (delay 500ms)
  END #2 (delay 300ms)
  END #3 (delay 400ms)
  END #4 (delay 500ms)
END main

Program exited.
```

[Playground](https://go.dev/play/p/EDgLh1A9tMR)

As you can see, 3 goroutines were immediately launched `#0`, `#1`, `#2`, then, after completing one of them (`#0`), the next one is started (`#3`) and so on.

The problem is solved.