Code
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
// 1.
func printNums(str string) time.Duration {
var total time.Duration
for i := range 100 {
wait := time.Duration(10+rand.Intn(20)) * time.Millisecond
time.Sleep(wait)
fmt.Println(str, i, wait)
total += wait
}
return total
}
// 2.
func printNumsAlt(str string, wg *sync.WaitGroup, ch chan<- time.Duration) {
defer wg.Done()
var total time.Duration
for i := range 100 {
wait := time.Duration(10+rand.Intn(20)) * time.Millisecond
time.Sleep(wait)
fmt.Println(str, i, wait)
total += wait
}
ch <- total
}
func main() {
// 3.
startNow1 := time.Now()
t1 := printNums("first call")
t2 := printNums("second call")
fmt.Printf("first total: %vms ", t1.Milliseconds())
fmt.Printf("second total: %vms\n", t2.Milliseconds())
startNow1Since := time.Since(startNow1).Milliseconds()
fmt.Printf("this operation took: %vms\n", startNow1Since)
// 4.
ch := make(chan time.Duration, 2)
// 5.
startNow2 := time.Now()
var wg sync.WaitGroup
wg.Add(1)
go printNumsAlt("first go routine", &wg, ch)
wg.Add(1)
go printNumsAlt("second go routine", &wg, ch)
// 6.
fmt.Println("this message will be seen with the go routines")
// 7.
go func() {
wg.Wait()
close(ch)
fmt.Println("this message will be seen when all routines are done")
}()
// 8.
var duration time.Duration
for d := range ch {
duration += d
fmt.Println("Received:", d.Milliseconds())
}
// 9.
startNow2Since := time.Since(startNow2).Milliseconds()
fmt.Printf("this operation took: %vms\n", startNow2Since)
fmt.Printf("one go routine %vms vs many go routines %vms\n", startNow1Since, startNow2Since)
// fmt.Printf("first total: %vms\n", t1.Milliseconds())
// fmt.Printf("second total %vms\n", t2.Milliseconds())
}Notes
quote “Concurrency is about dealing with lot of things at once, Parallelism is about doing a lot of things at once” - Rob Pike
- These functions simulate API calls and how they can reliably take a bit of time to respond if all is running normally.
- This function does not return anything and instead puts the results in a channel
- Calling the functions in the main go routine process is the usual way to call functions, Go always has at least one go routine running, this call to
printNumshas to finish before the second call can start and the data can be processed. ThestartNow1variable will show that the total amount of time it takes to run these two functions is equal to $t1 + t2$. - This bi-directional channel gets results from the functions that are going to be called in different go routines, it accepts
time.Durationvalues and has a buffer size of 2 which means it can collect 2 results and then it will be full. In the function we use the channel, it is set to send-only which help keep the use of the channel in that function clear. - Using separate go routines you can run functions that would normally block the execution of further functions - we create a
WaitGroupwhich allows us to track how many go routines we have by using thewg.Addmethod.- Along with
wg.Waitwhich tells the main go routine to wait until the wait group is back down to 0.printNumsAlthas a deferwg.Donemethod which is what removes 1 from the wait group, when all of the go routines have finished, the rest of the program can execute. - The
startNow2variable will show the total amount of time it takes to run - these two functions will be equal to just above $(t3 + t4) / 2$ on my computer.
- Along with
- Since this message comes before the
Waitmethod, it will also be called as the other go routines are called, since those routines are no longer running on the main go routine anymore so there is no reason for it not to run.- It also shows why the
Waitmethod call is needed, if there were only a few quick things left to do, the main go routine would finish and close before the other go routines had finished and their values would be lost, the other message waits until theWaitmethod is called before executing.
- It also shows why the
- This anonymous function is also a go routine which waits for all of the other go routines to finish and closes the channel, the
Waitmethod will be called when the wait group is back down to 0 and then the channel will close. - To get the results from the channel, we can put them into variables like normal:
t3 := <-chandt4 := <-chand then print them:fmt.Printf("first go routine: %vms ", t3.Milliseconds())andfmt.Printf("second go routine: %vms\n", t4.Milliseconds()). Or we can loop over the channel itself and get the values out, which can be safer if the channel grows in size as the loop will take care of this change for us and put the results in one variable. - On average the first two function calls to
printNums takes twice as long as the calls toprintNumsAltbecause Go can utilise concurrency and parallelism to complete the tasks together. Your CPU will change how Go does this, sometimes it will use concurrency which can be thought of as one person cooking and switching between tasks, they might start boiling the water, then chop some vegetables, then put some food in the oven, then go back to the water and put the vegetables in the pan. Parallelism can be thought of as many cooks in a kitchen doing separate tasks. Both have their pros and cons, if one cook is waiting for another to finish a task, that can actually take longer than one person doing all the tasks if they are efficient, it all depends on the tasks.
Sources
Entries in my collection of notes are fairly informal, there may be inaccuracies here and they could be moved or changed at any time.