-
Notifications
You must be signed in to change notification settings - Fork 27
/
stateful-goroutines.go
125 lines (110 loc) · 3.59 KB
/
stateful-goroutines.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
package main
import (
"fmt"
"math/rand"
"sync/atomic"
"time"
)
/*
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.
*/
/*
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 structs 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
}
// StatefulGoRoutines to illustrate channel-based approach align with Go's ideas of sharing memory
func StatefulGoRoutines() {
// As before we’ll count how many operations we perform.
var readOps uint64
var writeOps uint64
// 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,
which is a map as in the previous example but now private to the stateful goroutine.
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() {
var state = make(map[int]int)
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.AddUint64(&readOps, 1)
time.Sleep(time.Millisecond)
}
}()
}
// 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.AddUint64(&writeOps, 1)
time.Sleep(time.Millisecond)
}
}()
}
// Let the goroutines work for a second.
time.Sleep(time.Second)
// Finally, capture and report the op counts.
readOpsFinal := atomic.LoadUint64(&readOps)
fmt.Println("readOps:", readOpsFinal)
writeOpsFinal := atomic.LoadUint64(&writeOps)
fmt.Println("writeOps:", writeOpsFinal)
// Running our program shows that the goroutine-based state
// management example completes about 80,000 total operations.
/*
For this particular case the goroutine-based approach was a bit more involved than the mutex-based one.
It might be useful in certain cases though,
for example where you have other channels involved or when managing multiple such mutexes would be error-prone.
You should use whichever approach feels most natural,
especially with respect to understanding the correctness of your program.
*/
}