In the previous example we used explicit locking with
mutexes to synchronize access to shared state across
multiple goroutines. Another option is to use the
built-in synchronization features of goroutines and
channels to achieve the same result. This channel-based
approach aligns with Go’s ideas of sharing memory by
communicating and having each piece of data owned
by exactly 1 goroutine.
|
|
|
|
|
import (
"fmt"
"math/rand"
"sync/atomic"
"time"
)
|
In this example our state will be owned by a single
goroutine. This will guarantee that the data is never
corrupted with concurrent access. In order to read or
write that state, other goroutines will send messages
to the owning goroutine and receive corresponding
replies. These readOp and writeOp struct s
encapsulate those requests and a way for the owning
goroutine to respond.
|
type readOp struct {
key int
resp chan int
}
type writeOp struct {
key int
val int
resp chan bool
}
|
|
|
The state will be a map as in the previous
example.
|
var state = make(map[int]int)
|
Also as before we’ll count how many operations we
perform.
|
|
The reads and writes channels will be used by
other goroutines to issue read and write requests,
respectively.
|
reads := make(chan *readOp)
writes := make(chan *writeOp)
|
Here is the goroutine that owns the state . This
goroutine repeatedly selects on the reads and
writes channels, responding to requests as they
arrive. A response is executed by first performing
the requested operation and then sending a value
on the response channel resp to indicate success
(and the desired value in the case of reads ).
|
go func() {
for {
select {
case read := <-reads:
read.resp <- state[read.key]
case write := <-writes:
state[write.key] = write.val
write.resp <- true
}
}
}()
|
This starts 100 goroutines to issue reads to the
state-owning goroutine via the reads channel.
Each read requires constructing a readOp , sending
it over the reads channel, and the receiving the
result over the provided resp channel.
|
for r := 0; r < 100; r++ {
go func() {
for {
read := &readOp{
key: rand.Intn(5),
resp: make(chan int)}
reads <- read
<-read.resp
atomic.AddInt64(&ops, 1)
}
}()
}
|
We start 10 writes as well, using a similar
approach.
|
for w := 0; w < 10; w++ {
go func() {
for {
write := &writeOp{
key: rand.Intn(5),
val: rand.Intn(100),
resp: make(chan bool)}
writes <- write
<-write.resp
atomic.AddInt64(&ops, 1)
}
}()
}
|
Let the goroutines work for a second.
|
|
Finally, capture and report the ops count.
|
opsFinal := atomic.LoadInt64(&ops)
fmt.Println("ops:", opsFinal)
}
|