From f09cadcbd999c8433cc94da256f7f626190b74c3 Mon Sep 17 00:00:00 2001
From: Eli Bendersky
Date: Sat, 30 Oct 2021 06:20:02 -0700
Subject: [PATCH] Change mutexes example to make it simpler and more
predictable
Fixes #364
---
examples/mutexes/mutexes.go | 122 ++++++++----------
examples/mutexes/mutexes.hash | 4 +-
examples/mutexes/mutexes.sh | 9 +-
public/mutexes | 231 +++++++++++++---------------------
4 files changed, 142 insertions(+), 224 deletions(-)
diff --git a/examples/mutexes/mutexes.go b/examples/mutexes/mutexes.go
index 59e736d..fce6019 100644
--- a/examples/mutexes/mutexes.go
+++ b/examples/mutexes/mutexes.go
@@ -7,79 +7,57 @@ package main
import (
"fmt"
- "math/rand"
"sync"
- "sync/atomic"
- "time"
)
-func main() {
-
- // For our example the `state` will be a map.
- var state = make(map[int]int)
-
- // This `mutex` will synchronize access to `state`.
- var mutex = &sync.Mutex{}
-
- // We'll keep track of how many read and write
- // operations we do.
- var readOps uint64
- var writeOps uint64
-
- // Here we start 100 goroutines to execute repeated
- // reads against the state, once per millisecond in
- // each goroutine.
- for r := 0; r < 100; r++ {
- go func() {
- total := 0
- for {
-
- // For each read we pick a key to access,
- // `Lock()` the `mutex` to ensure
- // exclusive access to the `state`, read
- // the value at the chosen key,
- // `Unlock()` the mutex, and increment
- // the `readOps` count.
- key := rand.Intn(5)
- mutex.Lock()
- total += state[key]
- mutex.Unlock()
- atomic.AddUint64(&readOps, 1)
-
- // Wait a bit between reads.
- time.Sleep(time.Millisecond)
- }
- }()
- }
-
- // We'll also start 10 goroutines to simulate writes,
- // using the same pattern we did for reads.
- for w := 0; w < 10; w++ {
- go func() {
- for {
- key := rand.Intn(5)
- val := rand.Intn(100)
- mutex.Lock()
- state[key] = val
- mutex.Unlock()
- atomic.AddUint64(&writeOps, 1)
- time.Sleep(time.Millisecond)
- }
- }()
- }
-
- // Let the 10 goroutines work on the `state` and
- // `mutex` for a second.
- time.Sleep(time.Second)
-
- // Take and report final operation counts.
- readOpsFinal := atomic.LoadUint64(&readOps)
- fmt.Println("readOps:", readOpsFinal)
- writeOpsFinal := atomic.LoadUint64(&writeOps)
- fmt.Println("writeOps:", writeOpsFinal)
-
- // With a final lock of `state`, show how it ended up.
- mutex.Lock()
- fmt.Println("state:", state)
- mutex.Unlock()
+// Container holds a map of counters; since we want to
+// update it concurrently from multiple goroutines, we
+// add a `Mutex` to synchronize access. The mutex is
+// _embedded_ in this `struct`; this is idiomatic in Go.
+// Note that mutexes must not be copied, so if this
+// `struct` is passed around, it should be done by
+// pointer.
+type Container struct {
+ sync.Mutex
+ counters map[string]int
+}
+
+func (c *Container) inc(name string) {
+ // Lock the mutex before accessing `counters`; unlock
+ // it at the end of the function using a [defer](defer)
+ // statement. Since the mutex is embedded into
+ // `Container`, we can call the mutex's methods like
+ // `Lock` directly on `c`.
+ c.Lock()
+ defer c.Unlock()
+ c.counters[name]++
+}
+
+func main() {
+ c := Container{
+ counters: map[string]int{"a": 0, "b": 0},
+ }
+
+ var wg sync.WaitGroup
+
+ // This function increments a named counter
+ // in a loop.
+ doIncrement := func(name string, n int) {
+ for i := 0; i < n; i++ {
+ c.inc(name)
+ }
+ wg.Done()
+ }
+
+ // Run several goroutines concurrently; note
+ // that they all access the same `Container`,
+ // and two of them access the same counter.
+ wg.Add(3)
+ go doIncrement("a", 10000)
+ go doIncrement("a", 10000)
+ go doIncrement("b", 10000)
+
+ // Wait a for the goroutines to finish
+ wg.Wait()
+ fmt.Println(c.counters)
}
diff --git a/examples/mutexes/mutexes.hash b/examples/mutexes/mutexes.hash
index 4455e8c..70c6f65 100644
--- a/examples/mutexes/mutexes.hash
+++ b/examples/mutexes/mutexes.hash
@@ -1,2 +1,2 @@
-253b089b8145fc57a90ae4024346b6db2ec1659b
-CHCDredHCOz
+07179e54fb3466ab01ac8aa9550feb213a206785
+i50fhu4l-n0
diff --git a/examples/mutexes/mutexes.sh b/examples/mutexes/mutexes.sh
index 185f4fa..d379c1f 100644
--- a/examples/mutexes/mutexes.sh
+++ b/examples/mutexes/mutexes.sh
@@ -1,10 +1,7 @@
-# Running the program shows that we executed about
-# 90,000 total operations against our `mutex`-synchronized
-# `state`.
+# Running the program shows that the counters
+# updated as expected.
$ go run mutexes.go
-readOps: 83285
-writeOps: 8320
-state: map[1:97 4:53 0:33 2:15 3:2]
+map[a:20000 b:10000]
# Next we'll look at implementing this same state
# management task using only goroutines and channels.
diff --git a/public/mutexes b/public/mutexes
index 0624b0a..4fd5e95 100644
--- a/public/mutexes
+++ b/public/mutexes
@@ -44,7 +44,7 @@ to safely access data across multiple goroutines.
- 
+ 
package main
|
@@ -58,15 +58,64 @@ to safely access data across multiple goroutines.
import (
"fmt"
- "math/rand"
"sync"
- "sync/atomic"
- "time"
)
+
+
+ Container holds a map of counters; since we want to
+update it concurrently from multiple goroutines, we
+add a Mutex to synchronize access. The mutex is
+embedded in this struct ; this is idiomatic in Go.
+Note that mutexes must not be copied, so if this
+struct is passed around, it should be done by
+pointer.
+
+ |
+
+
+
+type Container struct {
+ sync.Mutex
+ counters map[string]int
+}
+
+ |
+
+
+
+
+ Lock the mutex before accessing counters ; unlock
+it at the end of the function using a defer
+statement. Since the mutex is embedded into
+Container , we can call the mutex’s methods like
+Lock directly on c .
+
+ |
+
+
+ func (c *Container) inc(name string) {
+
+ |
+
+
+
+
+
+ |
+
+
+ c.Lock()
+ defer c.Unlock()
+ c.counters[name]++
+}
+
+ |
+
+
@@ -74,102 +123,8 @@ to safely access data across multiple goroutines.
|
func main() {
-
- |
-
-
-
-
- For our example the state will be a map.
-
- |
-
-
-
- var state = make(map[int]int)
-
- |
-
-
-
-
- This mutex will synchronize access to state .
-
- |
-
-
-
- var mutex = &sync.Mutex{}
-
- |
-
-
-
-
- We’ll keep track of how many read and write
-operations we do.
-
- |
-
-
-
- var readOps uint64
- var writeOps uint64
-
- |
-
-
-
-
- Here we start 100 goroutines to execute repeated
-reads against the state, once per millisecond in
-each goroutine.
-
- |
-
-
-
- for r := 0; r < 100; r++ {
- go func() {
- total := 0
- for {
-
- |
-
-
-
-
- For each read we pick a key to access,
-Lock() the mutex to ensure
-exclusive access to the state , read
-the value at the chosen key,
-Unlock() the mutex, and increment
-the readOps count.
-
- |
-
-
-
- key := rand.Intn(5)
- mutex.Lock()
- total += state[key]
- mutex.Unlock()
- atomic.AddUint64(&readOps, 1)
-
- |
-
-
-
-
- Wait a bit between reads.
-
- |
-
-
-
- time.Sleep(time.Millisecond)
- }
- }()
+ c := Container{
+ counters: map[string]int{"a": 0, "b": 0},
}
|
@@ -177,25 +132,29 @@ the readOps
count.
- We’ll also start 10 goroutines to simulate writes,
-using the same pattern we did for reads.
+
+ |
+
+
+ var wg sync.WaitGroup
+
+ |
+
+
+
+
+ This function increments a named counter
+in a loop.
|
- for w := 0; w < 10; w++ {
- go func() {
- for {
- key := rand.Intn(5)
- val := rand.Intn(100)
- mutex.Lock()
- state[key] = val
- mutex.Unlock()
- atomic.AddUint64(&writeOps, 1)
- time.Sleep(time.Millisecond)
- }
- }()
+ doIncrement := func(name string, n int) {
+ for i := 0; i < n; i++ {
+ c.inc(name)
+ }
+ wg.Done()
}
|
@@ -203,45 +162,32 @@ using the same pattern we did for reads.
- Let the 10 goroutines work on the state and
-mutex for a second.
+ Run several goroutines concurrently; note
+that they all access the same Container ,
+and two of them access the same counter.
|
- time.Sleep(time.Second)
+ wg.Add(3)
+ go doIncrement("a", 10000)
+ go doIncrement("a", 10000)
+ go doIncrement("b", 10000)
|
- Take and report final operation counts.
-
- |
-
-
-
- readOpsFinal := atomic.LoadUint64(&readOps)
- fmt.Println("readOps:", readOpsFinal)
- writeOpsFinal := atomic.LoadUint64(&writeOps)
- fmt.Println("writeOps:", writeOpsFinal)
-
- |
-
-
-
-
- With a final lock of state , show how it ended up.
+ Wait a for the goroutines to finish
|
- mutex.Lock()
- fmt.Println("state:", state)
- mutex.Unlock()
+ wg.Wait()
+ fmt.Println(c.counters)
}
|
@@ -253,18 +199,15 @@ using the same pattern we did for reads.
- Running the program shows that we executed about
-90,000 total operations against our mutex -synchronized
-state .
+ Running the program shows that the counters
+updated as expected.
|
$ go run mutexes.go
-readOps: 83285
-writeOps: 8320
-state: map[1:97 4:53 0:33 2:15 3:2]
+map[a:20000 b:10000]
|
@@ -295,7 +238,7 @@ management task using only goroutines and channels.