From ef8f0e3831c3b888c390e0734fde4c31ee8e386d Mon Sep 17 00:00:00 2001 From: Eli Bendersky Date: Thu, 5 Sep 2019 10:50:09 -0700 Subject: [PATCH 1/2] Deterministic example for atomics. Fixes #265 --- examples/atomic-counters/atomic-counters.go | 39 +++++----- examples/atomic-counters/atomic-counters.hash | 4 +- examples/atomic-counters/atomic-counters.sh | 10 ++- public/atomic-counters | 77 ++++++++++--------- 4 files changed, 66 insertions(+), 64 deletions(-) diff --git a/examples/atomic-counters/atomic-counters.go b/examples/atomic-counters/atomic-counters.go index 9df3276..2a0901b 100644 --- a/examples/atomic-counters/atomic-counters.go +++ b/examples/atomic-counters/atomic-counters.go @@ -7,9 +7,11 @@ package main -import "fmt" -import "time" -import "sync/atomic" +import ( + "fmt" + "sync" + "sync/atomic" +) func main() { @@ -17,33 +19,28 @@ func main() { // (always-positive) counter. var ops uint64 - // To simulate concurrent updates, we'll start 50 - // goroutines that each increment the counter about - // once a millisecond. + // A WaitGroup will help us wait for all goroutines + // to finish their work. + var wg sync.WaitGroup + + // We'll start 50 goroutines that each increment the + // counter exactly 1000 times. for i := 0; i < 50; i++ { + wg.Add(1) + go func() { - for { + for c := 0; c < 1000; c++ { // To atomically increment the counter we // use `AddUint64`, giving it the memory // address of our `ops` counter with the // `&` syntax. atomic.AddUint64(&ops, 1) - - // Wait a bit between increments. - time.Sleep(time.Millisecond) } + wg.Done() }() } - // Wait a second to allow some ops to accumulate. - time.Sleep(time.Second) - - // In order to safely use the counter while it's still - // being updated by other goroutines, we extract a - // copy of the current value into `opsFinal` via - // `LoadUint64`. As above we need to give this - // function the memory address `&ops` from which to - // fetch the value. - opsFinal := atomic.LoadUint64(&ops) - fmt.Println("ops:", opsFinal) + // Wait until all the goroutines are done. + wg.Wait() + fmt.Println("ops:", ops) } diff --git a/examples/atomic-counters/atomic-counters.hash b/examples/atomic-counters/atomic-counters.hash index c1b531a..e35f8f2 100644 --- a/examples/atomic-counters/atomic-counters.hash +++ b/examples/atomic-counters/atomic-counters.hash @@ -1,2 +1,2 @@ -a4190094ea0405b5f2733101beb97939a1d43aee -KDr9EMMPMgi +103c9b7d036e3a5c14dc481755b78b10dc9f894e +GRkVf6J1--B diff --git a/examples/atomic-counters/atomic-counters.sh b/examples/atomic-counters/atomic-counters.sh index e4523f9..1680a10 100644 --- a/examples/atomic-counters/atomic-counters.sh +++ b/examples/atomic-counters/atomic-counters.sh @@ -1,7 +1,11 @@ -# Running the program shows that we executed about -# 40,000 operations. +# We expect to get exactly 50,000 operations. Had we +# used the non-atomic `ops++` to increment the counter, +# we'd likely get a different number, changing between +# runs, because the goroutines would interfere with +# each other. Moreover, we'd get data race failures +# when running with the `-race` flag. $ go run atomic-counters.go -ops: 41419 +ops: 50000 # Next we'll look at mutexes, another tool for managing # state. diff --git a/public/atomic-counters b/public/atomic-counters index b2e006e..6c9d94c 100644 --- a/public/atomic-counters +++ b/public/atomic-counters @@ -46,7 +46,7 @@ counters accessed by multiple goroutines.

- +
package main
 
@@ -59,9 +59,11 @@ counters accessed by multiple goroutines.

-
import "fmt"
-import "time"
-import "sync/atomic"
+          
import (
+    "fmt"
+    "sync"
+    "sync/atomic"
+)
 
@@ -95,16 +97,28 @@ counters accessed by multiple goroutines.

-

To simulate concurrent updates, we’ll start 50 -goroutines that each increment the counter about -once a millisecond.

+

A WaitGroup will help us wait for all goroutines +to finish their work.

+ + + + +
    var wg sync.WaitGroup
+
+ + + + + + +

We’ll start 50 goroutines that each increment the +counter exactly 1000 times.

    for i := 0; i < 50; i++ {
-        go func() {
-            for {
+        wg.Add(1)
 
@@ -120,7 +134,8 @@ address of our ops counter with the -
                atomic.AddUint64(&ops, 1)
+          
        go func() {
+            for c := 0; c < 1000; c++ {
 
@@ -128,13 +143,13 @@ address of our ops counter with the -

Wait a bit between increments.

- + -
                time.Sleep(time.Millisecond)
+          
                atomic.AddUint64(&ops, 1)
             }
+            wg.Done()
         }()
     }
 
@@ -144,31 +159,13 @@ address of our ops counter with the -

Wait a second to allow some ops to accumulate.

- - - - -
    time.Sleep(time.Second)
-
- - - - - - -

In order to safely use the counter while it’s still -being updated by other goroutines, we extract a -copy of the current value into opsFinal via -LoadUint64. As above we need to give this -function the memory address &ops from which to -fetch the value.

+

Wait until all the goroutines are done.

-
    opsFinal := atomic.LoadUint64(&ops)
-    fmt.Println("ops:", opsFinal)
+          
    wg.Wait()
+    fmt.Println("ops:", ops)
 }
 
@@ -181,14 +178,18 @@ fetch the value.

-

Running the program shows that we executed about -40,000 operations.

+

We expect to get exactly 50,000 operations. Had we +used the non-atomic ops++ to increment the counter, +we’d likely get a different number, changing between +runs, because the goroutines would interfere with +each other. Moreover, we’d get data race failures +when running with the -race flag.

$ go run atomic-counters.go
-ops: 41419
+ops: 50000
 
@@ -219,7 +220,7 @@ state.

From b70c15adaa0983b3d07312f425b8c6d4df71457f Mon Sep 17 00:00:00 2001 From: Eli Bendersky Date: Thu, 5 Sep 2019 13:26:08 -0700 Subject: [PATCH 2/2] Clarify reading op non-atomically --- examples/atomic-counters/atomic-counters.go | 6 +++++ examples/atomic-counters/atomic-counters.hash | 4 ++-- public/atomic-counters | 24 +++++++++++++++---- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/examples/atomic-counters/atomic-counters.go b/examples/atomic-counters/atomic-counters.go index 2a0901b..046a347 100644 --- a/examples/atomic-counters/atomic-counters.go +++ b/examples/atomic-counters/atomic-counters.go @@ -42,5 +42,11 @@ func main() { // Wait until all the goroutines are done. wg.Wait() + + // It's safe to access `ops` now because we know + // no other goroutine is writing to it. Reading + // atomics safely while they are being updated is + // also possible, using functions like + // `atomic.LoadUint64`. fmt.Println("ops:", ops) } diff --git a/examples/atomic-counters/atomic-counters.hash b/examples/atomic-counters/atomic-counters.hash index e35f8f2..989ed19 100644 --- a/examples/atomic-counters/atomic-counters.hash +++ b/examples/atomic-counters/atomic-counters.hash @@ -1,2 +1,2 @@ -103c9b7d036e3a5c14dc481755b78b10dc9f894e -GRkVf6J1--B +8ebec0be3b167021c96b8b497d0e8c0a2ea99385 +F2pJfduyQiA diff --git a/public/atomic-counters b/public/atomic-counters index 6c9d94c..9627c97 100644 --- a/public/atomic-counters +++ b/public/atomic-counters @@ -46,7 +46,7 @@ counters accessed by multiple goroutines.

- +
package main
 
@@ -162,10 +162,26 @@ address of our ops counter with the

Wait until all the goroutines are done.

- +
    wg.Wait()
-    fmt.Println("ops:", ops)
+
+ + + + + + +

It’s safe to access ops now because we know +no other goroutine is writing to it. Reading +atomics safely while they are being updated is +also possible, using functions like +atomic.LoadUint64.

+ + + + +
    fmt.Println("ops:", ops)
 }
 
@@ -220,7 +236,7 @@ state.