DEV Community

Aleksey Reprintsev
Aleksey Reprintsev

Posted on

How to limit the number of simultaneously running goroutines and wait for their completion

Table Of Contents

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

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 and here.

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")
}
Enter fullscreen mode Exit fullscreen mode

Output:

START main
END main

Program exited.
Enter fullscreen mode Exit fullscreen mode

Playground

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()

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

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")
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Playground

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

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 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.

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")
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Playground

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

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.

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")
}
Enter fullscreen mode Exit fullscreen mode

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.
Enter fullscreen mode Exit fullscreen mode

Playground

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.

Top comments (0)