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.