Golang: Concurrency is Hard; So What Can We Do About It?

dm03514
dm03514
Oct 7, 2018 · 14 min read

Concurrency In GO

Dangers Of Concurrency

Race Conditions

reqCount := Counter{}

http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
value := reqCount.Value()
fmt.Printf("handling request: %d\n", value)
time.Sleep(1 * time.Nanosecond)
reqCount.Set(value + 1)
fmt.Fprintln(w, "Hello, client")
}))
log.Fatal(http.ListenAndServe(":8080", nil))
$ go test -run TestExplicitRace ./races/ -v -total-requests=200 -concurrent-requests=200
...
handling request: 9
handling request: 9
handling request: 9
handling request: 10
handling request: 11
handling request: 12
handling request: 13
handling request: 14
handling request: 15
handling request: 16
handling request: 17
handling request: 18
handling request: 19
handling request: 20
handling request: 21
handling request: 22
handling request: 23
handling request: 24
handling request: 25
handling request: 26
handling request: 26
handling request: 26
handling request: 26
Num Requests TO Make: 200
Final Count: 27
--- FAIL: TestExplicitRace (0.08s)
explict_test.go:72: expected 200 requests: received 27
FAIL
FAIL github.com/dm03514/grokking-go/candidates-and-contexts/races 0.083s
value := reqCount.Value()
time.Sleep(1 * time.Nanosecond)
reqCount.Set(value + 1)
$ go test -run TestLogicalRace ./races/ -v -total-requests=200 -concurrent-requests=200...
handling request: 25
handling request: 25
handling request: 25
handling request: 25
handling request: 25
handling request: 25
handling request: 25
handling request: 25
handling request: 25
handling request: 25
handling request: 20
handling request: 25
handling request: 26
handling request: 27
Num Requests TO Make: 200
Final Count: 26
--- FAIL: TestLogicalRace (0.12s)
logical_test.go:67: expected 200 requests: received 26
FAIL
FAIL github.com/dm03514/grokking-go/candidates-and-contexts/races 0.123s

Solutions

Race Detector

$ go test -run TestExplicitRace ./races/ -v -total-requests=200 -concurrent-requests=200 -race=== RUN   TestExplicitRace
handling request: 0
handling request: 0
handling request: 0
handling request: 0
==================
WARNING: DATA RACE
Write at 0x00c4200164e8 by goroutine 326:
github.com/dm03514/grokking-go/candidates-and-contexts/races.TestExplicitRace.func1.1()
/vagrant_data/go/src/github.com/dm03514/grokking-go/candidates-and-contexts/races/counters.go:18 +0x115
net/http.HandlerFunc.ServeHTTP()
/usr/local/go/src/net/http/server.go:1947 +0x51
net/http.(*ServeMux).ServeHTTP()
/usr/local/go/src/net/http/server.go:2340 +0x9f
net/http.serverHandler.ServeHTTP()
/usr/local/go/src/net/http/server.go:2697 +0xb9
net/http.(*conn).serve()
/usr/local/go/src/net/http/server.go:1830 +0x7dc
Previous read at 0x00c4200164e8 by goroutine 426:
github.com/dm03514/grokking-go/candidates-and-contexts/races.TestExplicitRace.func1.1()
/vagrant_data/go/src/github.com/dm03514/grokking-go/candidates-and-contexts/races/counters.go:14 +0x5b
net/http.HandlerFunc.ServeHTTP()
/usr/local/go/src/net/http/server.go:1947 +0x51
net/http.(*ServeMux).ServeHTTP()
/usr/local/go/src/net/http/server.go:2340 +0x9f
net/http.serverHandler.ServeHTTP()
/usr/local/go/src/net/http/server.go:2697 +0xb9
net/http.(*conn).serve()
/usr/local/go/src/net/http/server.go:1830 +0x7dc
Goroutine 326 (running) created at:
net/http.(*Server).Serve()
/usr/local/go/src/net/http/server.go:2798 +0x364
net/http.(*Server).ListenAndServe()
/usr/local/go/src/net/http/server.go:2714 +0xc4
net/http.ListenAndServe()
/usr/local/go/src/net/http/server.go:2972 +0xf6
github.com/dm03514/grokking-go/candidates-and-contexts/races.TestExplicitRace.func1()
/vagrant_data/go/src/github.com/dm03514/grokking-go/candidates-and-contexts/races/explict_test.go:36 +0xd9
Goroutine 426 (running) created at:
net/http.(*Server).Serve()
/usr/local/go/src/net/http/server.go:2798 +0x364
net/http.(*Server).ListenAndServe()
/usr/local/go/src/net/http/server.go:2714 +0xc4
net/http.ListenAndServe()
/usr/local/go/src/net/http/server.go:2972 +0xf6
github.com/dm03514/grokking-go/candidates-and-contexts/races.TestExplicitRace.func1()
/vagrant_data/go/src/github.com/dm03514/grokking-go/candidates-and-contexts/races/explict_test.go:36 +0xd9
==================
WARNING: DATA RACE
Write at 0x00c4200164e8 by goroutine 326:
github.com/dm03514/grokking-go/candidates-and-contexts/races.TestExplicitRace.func1.1()
/vagrant_data/go/src/github.com/dm03514/grokking-go/candidates-and-contexts/races/counters.go:18 +0x115
...
Previous read at 0x00c4200164e8 by goroutine 426:
github.com/dm03514/grokking-go/candidates-and-contexts/races.TestExplicitRace.func1.1()
/vagrant_data/go/src/github.com/dm03514/grokking-go/candidates-and-contexts/races/counters.go:14 +0x5b
package races

import "sync"

type Counter struct {
count int
}

func (c *Counter) Value() int {
return c.count # line 14
}

func (c *Counter) Set(v int) {
c.count = v # line 18
}

Explicit Synchronization

type SynchronizedCounter struct {
mu *sync.Mutex
count int
}

func (c *SynchronizedCounter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()

c.count++
}

func (c *SynchronizedCounter) Value() int {
c.mu.Lock()
defer c.mu.Unlock()

return c.count
}

func (c *SynchronizedCounter) Set(v int) {
c.mu.Lock()
defer c.mu.Unlock()

c.count = v
}

Static Analysis (go vet)

type MisSynchronizedCounter struct {
mu sync.Mutex
count int
}

func (c MisSynchronizedCounter) Inc() {
c.mu.Lock()
defer c.mu.Unlock()

c.count++
}
$ go vet -copylocks ./races/counters.go
# command-line-arguments
races/counters.go:52: Inc passes lock by value: races.MisSynchronizedCounter contains sync.Mutex

Design based

countChan := make(chan struct{})
go func() {
for range countChan {
reqCount.Inc()
fmt.Printf("handling request: %d\n", reqCount.Value())
}
}()
$ go test -run TestDesignNoRace ./races/ -v -total-requests=200 -concurrent-requests=200 -racehandling request: 1
...
handling request: 190
handling request: 191
handling request: 192
handling request: 193
handling request: 194
handling request: 195
handling request: 196
handling request: 197
handling request: 198
handling request: 199
handling request: 200
Num Requests TO Make: 200
Final Count: 200
--- PASS: TestDesignNoRace (0.64s)
PASS
ok github.com/dm03514/grokking-go/candidates-and-contexts/races 1.691s

Analysis

No Concurrency

Candidates and Contexts (C&C)

Candidates

Contexts

Analysis

Example

type Counter struct {
count int
}

func (c *Counter) Value() int {
return c.count
}

func (c *Counter) Set(v int) {
c.count = v
}
reqCount := Counter{}

http.Handle("/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
value := reqCount.Value()
fmt.Printf("handling request: %d\n", value)
time.Sleep(1 * time.Nanosecond)
reqCount.Set(value + 1)
fmt.Fprintln(w, "Hello, client")
}))
log.Fatal(http.ListenAndServe(":8080", nil))

Conclusion

Dm03514 Tech Blog

Dm03514’s Tech Blog

dm03514

Written by

dm03514

https://stackoverflow.com/users/594589/dm03514

Dm03514 Tech Blog

Dm03514’s Tech Blog