[A Tour of Go Study Notes] 09 concurrency
Understanding Go concurrency using the official tutorial
Entering the world of Golang, we first explore basic Golang usage using the official tutorial: A Tour of Go
.
This section introduces the powerful concurrency feature of Go.
Goroutine
Lightweight thread management operated by Go runtime.
Starting and executing a new Goroutine can be understood as creating a new Thread.
go f(x, y, z)
Using go
followed by a function call enables the specified function f
to run on a new Goroutine.
f
, x
, y
, z
are taken from the current Goroutine, and main
also executes within the same thread.
In the example below, say("world")
will start another execution sequence concurrently with the original one executing say("hello")
.
Usually, say("world")
will execute first, followed by say("hello")
upon completion.
However, if go
is added before say("world")
, changing it to go say("world")
, it will initiate a new Goroutine while the original sequence continues with say("hello")
.
Both threads will run concurrently, and after the main Goroutine finishes execution, other Goroutines will be forcefully closed.
package main
import (
"fmt"
"time"
)
func say(s string) {
for i := 0; i < 5; i++ {
time.Sleep(100 * time.Millisecond)
fmt.Println(s)
}
}
func main() {
go say("world")
say("hello")
}
/*
>>> world
>>> hello
>>> world
>>> hello
>>> hello
>>> world
>>> world
>>> hello
>>> hello
*/
Channels
Channels are typed communications that use <-
to send or receive values. They possess a blocking
nature, enabling threads to wait.
Similar to maps and slices, channels must be created before use.
ch := make(chan int)
As channels function like pipelines, sending or receiving halts until the other end is ready.
They can be used for synchronizing data content among Goroutines.
ch <- v // Send v to channel ch
v := <-ch // Receive a value from ch and assign it to v
The following example demonstrates summing numbers in a slice, distributing the task among two Goroutines.
The final result is computed only when both Goroutines complete their calculations.
package main
import "fmt"
func sum(s []int, ch chan int) {
sum := 0
for _, v := range s {
sum += v
}
ch <- sum // send sum to ch
}
func main() {
s := []int{7, 2, 8, -9, 4, 0}
ch := make(chan int)
go sum(s[:len(s)/2], ch)
go sum(s[len(s)/2:], ch)
x, y := <-ch, <-ch // receive from ch
fmt.Println(x, y, x+y)
}
/*
>>> -5 17 12
*/
Buffered Channels
Channels can have a buffer, where the buffer length is provided as the second argument during channel initialization.
Regular channels, as mentioned earlier, exhibit:
- Blocking the sender when data is pushed without a receiver.
- Blocking the receiver when pulling from an empty channel.
With buffering, blocking occurs only when the buffer is full.
In the following example, only after the 101st
data is pushed does the sender Goroutine start waiting:
ch := make(chan int, 100)
The buffer length determines how many elements the channel can store.
Sending more elements than the buffer length leads to a deadlock
.
package main
import "fmt"
func main() {
ch := make(chan int, 1)
ch <- 1
ch <- 2
fmt.Println(<-ch)
fmt.Println(<-ch)
}
/*
>>> fatal error: all goroutines are asleep - deadlock!
*/
No error occurs if values are retrieved.
package main
import "fmt"
func main() {
ch := make(chan int, 1)
ch <- 1
fmt.Println(<-ch)
ch <- 2
fmt.Println(<-ch)
}
/*
>>> 1
>>> 2
*/
Range and Close
Senders can close
a channel to signify no more values to be sent.
Receivers can test if a channel is closed using the second argument.
If there are no values to receive and the channel is closed, ok
is set to false
.
When a channel is iterated using a for loop like for i := range c
, it continuously receives values until the channel is closed.
package main
import (
"fmt"
)
func fibonacci(n int, c chan int) {
x, y := 0, 1
for i := 0; i < n; i++ {
c <- x
x, y = y, x+y
}
close(c)
}
func main() {
c := make(chan int, 10)
go fibonacci(cap(c), c)
for i := range c {
fmt.Println(i)
}
}
/*
>>> 0
>>> 1
>>> 1
>>> 2
>>> 3
>>> 5
>>> 8
>>> 13
>>> 21
>>> 34
*/
Sending data to a closed channel will cause a panic
.
Thus, the channel should be closed by the Goroutine that is sending data.
Additionally, channels differ from files; typically, they donβt need manual closing.
Closing is only necessary when it’s crucial to inform receivers that no new values will be sent, such as in a range
loop.
Select
Using select
, various scenarios involving channels can be handled, including handling during blocking.
select {
case i := <-c:
// use i
default:
// receiving from c would block
}
When none of the cases match, the default
case executes.
package main
import (
"fmt"
"time"
)
func main() {
tick := time.Tick(100 * time.Millisecond)
boom := time.After(500 * time.Millisecond)
for {
select {
case <-tick:
fmt.Println("tick.")
case <-boom:
fmt.Println("BOOM!")
return
default:
fmt.Println(" .")
time.Sleep(50 * time.Millisecond)
}
}
}
/*
>>> .
>>> .
>>> tick.
>>> .
>>> .
>>> tick.
>>> .
>>> .
>>> tick.
>>> .
>>> .
>>> tick.
>>> .
>>> .
>>> tick.
>>> BOOM!
*/
sync.Mutex
Previously, we used channels to facilitate communication and data transfer between Goroutines. Due to their blocking
nature, when pulling data without any, it waits for other Goroutines to push data into the channel.
However, what if we donβt use channels and directly share variables among multiple Goroutines?
When multiple Goroutines share a variable and simultaneously operate on it, a Race Condition occurs, where ensuring the correctness of computation results becomes challenging.
This involves the concept of mutual exclusion, which can be handled using a Mutex to prevent conflicts.
Go provides a struct: sync.Mutex , offering two methods:
- Lock
- Unlock
Operations between Lock
and Unlock
make other Goroutines wait.
package main
import (
"fmt"
"sync"
"time"
)
// SafeCounter is safe to use concurrently.
type SafeCounter struct {
mu sync.Mutex
v map[string]int
}
// Inc increments the counter for the given key.
func (c *SafeCounter) Inc(key string) {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
c.v[key]++
c.mu.Unlock()
}
// Value returns the current value of the counter for the given key.
func (c *SafeCounter) Value(key string) int {
c.mu.Lock()
// Lock so only one goroutine at a time can access the map c.v.
defer c.mu.Unlock()
return c.v[key]
}
func main() {
c := SafeCounter{v: make(map[string]int)}
for i := 0; i < 1000; i++ {
go c.Inc("somekey")
}
time.Sleep(time.Second)
fmt.Println(c.Value("somekey"))
}
/*
>>> 1000
*/