Goroutines, Channels and awaiting Asynchronously Operations in Go
Golang has fantastic support for actions that are supposed to happen concurrently (at the same time) without blocking the thread, they are called goroutines and are used by simply putting go
in front of a function.
The functions prefixed with go
will run "on their own" and the rest of your code will continue to run.
In order to gather results or returns
from the functions, you commonly make use of a channel. Channels are the collecting "buckets" that will receive what your goroutines write to them.
This example is taken from the Golang Bootcamp: Concurrency and annotated a bit more.
package main
// note the function needs to be passed the channel
func sum(a []int, c chan int) {
sum := 0
// iterate over array
for _, v := range a {
// add value to sum
sum += v
}
// send sum to c
c <- sum
}
func main() {
a := []int{7, 2, 8, -9, 4, 0}
c := make(chan int)
// send first half
go sum(a[:len(a)/2], c)
// send second half
go sum(a[len(a)/2:], c)
// receive from c to x and y
x, y := <-c, <-c
}
Not that c
, our channel, is initiated with a type. You can also specify your own types, but we'll look at that in a later example.
Channels and appending to Slices
When using channels, you can iterate over items as well and add them to an existing array usingappend
. Note that by default, channels will be waited for, if you only use one go
prefix in a loop, you'll basically have the same program as before:
package main
import (
"fmt"
"math/rand"
"time"
)
func randomInt(min int, max int) int {
var bytes int
bytes = min + rand.Intn(max)
return int(bytes)
}
func plusone(a int, c chan int) {
fmt.Printf("now processing: %v\n", a)
// different random delays, to show that this is synchronously executed
time.Sleep(time.Second * time.Duration(randomInt(0, 2)))
c <- a + 1
}
func main() {
a := []int{7, 2, 8}
c := make(chan int)
var incrementedIntegers []int
for _, element := range a {
go plusone(element, c)
// get result from channel
y := <-c
// append to existing array
incrementedIntegers = append(incrementedIntegers, y)
}
fmt.Println(incrementedIntegers)
}
Try changing the for loop to:
go plusone(a[0], c)
go plusone(a[1], c)
go plusone(a[2], c)
// append to existing array
incrementedIntegers = append(incrementedIntegers, <-c)
incrementedIntegers = append(incrementedIntegers, <-c)
incrementedIntegers = append(incrementedIntegers, <-c)
The expected output is still:
now processing: 8
now processing: 2
now processing: 7
[8 9 3]
This will make all your additions run at the same time, note that you need to receive from the channel as many times as you expect output.
Your fmt.Println
statement will wait from all values being read from the channel.
Obviously it's not practical to type out every index of an array in order to make your program do a number of things at the same time, let's have a look at how we can run a dynamic number of things at once and wait for them to be finished.
Go Channels and Awaiting Asynchronous Operations
With Node.js we often rely on theasync
npm module, but in Go some similar functionality is already built in through sync.WaitGroup
s. We can pass WaitGroups a goroutine have them tell us when they're done and have written to their channel.
package main
import (
"fmt"
"math/rand"
"sync"
"time"
)
func randomInt(min int, max int) int {
var bytes int
bytes = min + rand.Intn(max)
return int(bytes)
}
func plusone(a int, c chan int, waitGroup *sync.WaitGroup) {
defer waitGroup.Done()
fmt.Printf("now processing: %v\n", a)
// different random delays, to show that this is synchronously executed
time.Sleep(time.Second * time.Duration(randomInt(0, 2)))
c <- a + 1
}
func main() {
a := []int{7, 2, 8}
c := make(chan int)
var waitGroup sync.WaitGroup
var incrementedIntegers []int
for _, element := range a {
// add to waitGroup
waitGroup.Add(1)
go plusone(element, c, &waitGroup)
}
// append to existing array
for range a {
incrementedIntegers = append(incrementedIntegers, <-c)
}
fmt.Println("Waiting for every Goroutine to be done")
waitGroup.Wait()
fmt.Println(incrementedIntegers)
}
When running the code, you should see that the calculations start at once, but the result has to be waited for, since we still have our delay in the plusOne
function.
Summary
Go is a great language, built with a lot of real world problems in mind. It's a pleasure to develop with and explore their implementation of concurrency. I hope this post made them a bit more clear! Let me know what you think!If you at any point read once from a channel that has been written to more times than one (this goes for any two numbers where the writing exceeds the reads), you'll receive a deadlock error. Just double check you're reading all the results:
fatal error: all goroutines are asleep - deadlock!
goroutine 1 [semacquire]:
sync.runtime_Semacquire(0xc4200140ec)
/usr/local/go/src/runtime/sema.go:56 +0x39
sync.(*WaitGroup).Wait(0xc4200140e0)
/usr/local/go/src/sync/waitgroup.go:131 +0x72
main.main()
/home/jonathan/projects/go/src/channels-tutorial/append-waitgroup.go:47 +0x208
goroutine 5 [chan send]:
main.plusone(0x7, 0xc42001e0c0, 0xc4200140e0)
/home/jonathan/projects/go/src/channels-tutorial/append-waitgroup.go:21 +0x11d
created by main.main
/home/jonathan/projects/go/src/channels-tutorial/append-waitgroup.go:34 +0xf9
goroutine 7 [chan send]:
main.plusone(0x8, 0xc42001e0c0, 0xc4200140e0)
/home/jonathan/projects/go/src/channels-tutorial/append-waitgroup.go:21 +0x11d
created by main.main
/home/jonathan/projects/go/src/channels-tutorial/append-waitgroup.go:34 +0xf9
exit status 2