Recently, I was working on package that was doing network requests inside goroutines and I encountered an issue: the program was really fast to finish, but the results were awful. This was because the number of goroutines running at the same time was too high. As a result, the network was congested, too many sockets were opened on my laptop and the final performance was degraded: requests were slow or failing.
In order to keep the network healthy while maintaining some concurrency, I wanted to limit the number of goroutines making requests at the same time. Here is a sample main file to illustrate how you can control the maximum number of goroutines that are allowed to run concurrently.
package main import ( "flag" "fmt" "time" ) // Fake a long and difficult work. func DoWork() { time.Sleep(500 * time.Millisecond) } func main() { maxNbConcurrentGoroutines := flag.Int("maxNbConcurrentGoroutines", 5, "the number of goroutines that are allowed to run concurrently") nbJobs := flag.Int("nbJobs", 100, "the number of jobs that we need to do") flag.Parse() // Dummy channel to coordinate the number of concurrent goroutines. // This channel should be buffered otherwise we will be immediately blocked // when trying to fill it. concurrentGoroutines := make(chan struct{}, *maxNbConcurrentGoroutines) // Fill the dummy channel with maxNbConcurrentGoroutines empty struct. for i := 0; i < *maxNbConcurrentGoroutines; i++ { concurrentGoroutines <- struct{}{} } // The done channel indicates when a single goroutine has // finished its job. done := make(chan bool) // The waitForAllJobs channel allows the main program // to wait until we have indeed done all the jobs. waitForAllJobs := make(chan bool) // Collect all the jobs, and since the job is finished, we can // release another spot for a goroutine. go func() { for i := 0; i < *nbJobs; i++ { <-done // Say that another goroutine can now start. concurrentGoroutines <- struct{}{} } // We have collected all the jobs, the program // can now terminate waitForAllJobs <- true }() // Try to start nbJobs jobs for i := 1; i <= *nbJobs; i++ { fmt.Printf("ID: %v: waiting to launch!\n", i) // Try to receive from the concurrentGoroutines channel. When we have something, // it means we can start a new goroutine because another one finished. // Otherwise, it will block the execution until an execution // spot is available. <-concurrentGoroutines fmt.Printf("ID: %v: it's my turn!\n", i) go func(id int) { DoWork() fmt.Printf("ID: %v: all done!\n", id) done <- true }(i) } // Wait for all jobs to finish <-waitForAllJobs }
This file is available as a gist on GitHub if you find it more convenient.
Sample runs
For the command time go run concurrent.go -nbJobs 25 -maxNbConcurrentGoroutines 10
:
ID: 1: waiting to launch! ID: 1: it's my turn! ID: 2: waiting to launch! ID: 2: it's my turn! ID: 3: waiting to launch! ID: 3: it's my turn! ID: 4: waiting to launch! ID: 4: it's my turn! ID: 5: waiting to launch! ID: 5: it's my turn! ID: 6: waiting to launch! ID: 6: it's my turn! ID: 7: waiting to launch! ID: 7: it's my turn! ID: 8: waiting to launch! ID: 8: it's my turn! ID: 9: waiting to launch! ID: 9: it's my turn! ID: 10: waiting to launch! ID: 10: it's my turn! ID: 11: waiting to launch! ID: 1: all done! ID: 9: all done! ID: 11: it's my turn! ID: 12: waiting to launch! ID: 12: it's my turn! ID: 7: all done! ID: 13: waiting to launch! ID: 5: all done! ID: 13: it's my turn! ID: 14: waiting to launch! ID: 4: all done! ID: 14: it's my turn! ID: 8: all done! ID: 15: waiting to launch! ID: 15: it's my turn! ID: 16: waiting to launch! ID: 16: it's my turn! ID: 10: all done! ID: 17: waiting to launch! ID: 2: all done! ID: 17: it's my turn! ID: 18: waiting to launch! ID: 18: it's my turn! ID: 3: all done! ID: 19: waiting to launch! ID: 6: all done! ID: 19: it's my turn! ID: 20: waiting to launch! ID: 20: it's my turn! ID: 21: waiting to launch! ID: 20: all done! ID: 16: all done! ID: 17: all done! ID: 12: all done! ID: 21: it's my turn! ID: 19: all done! ID: 11: all done! ID: 14: all done! ID: 18: all done! ID: 15: all done! ID: 13: all done! ID: 22: waiting to launch! ID: 22: it's my turn! ID: 23: waiting to launch! ID: 23: it's my turn! ID: 24: waiting to launch! ID: 24: it's my turn! ID: 25: waiting to launch! ID: 25: it's my turn! ID: 24: all done! ID: 21: all done! ID: 22: all done! ID: 25: all done! ID: 23: all done! 0,28s user 0,05s system 18% cpu 1,762 total
For the command time go run concurrent.go -nbJobs 10 -maxNbConcurrentGoroutines 1
:
ID: 1: waiting to launch! ID: 1: it's my turn! ID: 2: waiting to launch! ID: 1: all done! ID: 2: it's my turn! ID: 3: waiting to launch! ID: 2: all done! ID: 3: it's my turn! ID: 4: waiting to launch! ID: 3: all done! ID: 4: it's my turn! ID: 5: waiting to launch! ID: 4: all done! ID: 5: it's my turn! ID: 6: waiting to launch! ID: 5: all done! ID: 6: it's my turn! ID: 7: waiting to launch! ID: 6: all done! ID: 7: it's my turn! ID: 8: waiting to launch! ID: 7: all done! ID: 8: it's my turn! ID: 9: waiting to launch! ID: 8: all done! ID: 9: it's my turn! ID: 10: waiting to launch! ID: 9: all done! ID: 10: it's my turn! ID: 10: all done! 0,32s user 0,03s system 6% cpu 5,274 total
Questions? Feedback? Hit me on Twitter @AntoineAugusti