diff --git a/v2/LICENSE b/v2/LICENSE new file mode 100644 index 0000000..f20dac8 --- /dev/null +++ b/v2/LICENSE @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2016 Tevin Zhang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/v2/bool.go b/v2/bool.go new file mode 100644 index 0000000..bf1eb86 --- /dev/null +++ b/v2/bool.go @@ -0,0 +1,87 @@ +// Package abool provides atomic Boolean type for cleaner code and +// better performance. +package abool + +import ( + "encoding/json" + "sync/atomic" +) + +// New creates an AtomicBool with default set to false. +func New() *AtomicBool { + return new(AtomicBool) +} + +// NewBool creates an AtomicBool with given default value. +func NewBool(ok bool) *AtomicBool { + ab := New() + if ok { + ab.Set() + } + return ab +} + +// AtomicBool is an atomic Boolean. +// Its methods are all atomic, thus safe to be called by multiple goroutines simultaneously. +// Note: When embedding into a struct one should always use *AtomicBool to avoid copy. +type AtomicBool int32 + +// Set sets the Boolean to true. +func (ab *AtomicBool) Set() { + atomic.StoreInt32((*int32)(ab), 1) +} + +// UnSet sets the Boolean to false. +func (ab *AtomicBool) UnSet() { + atomic.StoreInt32((*int32)(ab), 0) +} + +// IsSet returns whether the Boolean is true. +func (ab *AtomicBool) IsSet() bool { + return atomic.LoadInt32((*int32)(ab)) == 1 +} + +// IsNotSet returns whether the Boolean is false. +func (ab *AtomicBool) IsNotSet() bool { + return !ab.IsSet() +} + +// SetTo sets the boolean with given Boolean. +func (ab *AtomicBool) SetTo(yes bool) { + if yes { + atomic.StoreInt32((*int32)(ab), 1) + } else { + atomic.StoreInt32((*int32)(ab), 0) + } +} + +// SetToIf sets the Boolean to new only if the Boolean matches the old. +// Returns whether the set was done. +func (ab *AtomicBool) SetToIf(old, new bool) (set bool) { + var o, n int32 + if old { + o = 1 + } + if new { + n = 1 + } + return atomic.CompareAndSwapInt32((*int32)(ab), o, n) +} + +// MarshalJSON behaves the same as if the AtomicBool is a builtin.bool. +// NOTE: There's no lock during the process, usually it shouldn't be called with other methods in parallel. +func (ab *AtomicBool) MarshalJSON() ([]byte, error) { + return json.Marshal(ab.IsSet()) +} + +// UnmarshalJSON behaves the same as if the AtomicBool is a builtin.bool. +// NOTE: There's no lock during the process, usually it shouldn't be called with other methods in parallel. +func (ab *AtomicBool) UnmarshalJSON(b []byte) error { + var v bool + err := json.Unmarshal(b, &v) + + if err == nil { + ab.SetTo(v) + } + return err +} diff --git a/v2/bool_test.go b/v2/bool_test.go new file mode 100644 index 0000000..d418c48 --- /dev/null +++ b/v2/bool_test.go @@ -0,0 +1,265 @@ +package abool + +import ( + "encoding/json" + "sync" + "sync/atomic" + "testing" +) + +func TestDefaultValue(t *testing.T) { + t.Parallel() + v := New() + if v.IsSet() { + t.Fatal("Empty value of AtomicBool should be false") + } + + v = NewBool(true) + if !v.IsSet() { + t.Fatal("NewValue(true) should be true") + } + + v = NewBool(false) + if v.IsSet() { + t.Fatal("NewValue(false) should be false") + } +} + +func TestIsNotSet(t *testing.T) { + t.Parallel() + v := New() + + if v.IsSet() == v.IsNotSet() { + t.Fatal("AtomicBool.IsNotSet() should be the opposite of IsSet()") + } +} + +func TestSetUnSet(t *testing.T) { + t.Parallel() + v := New() + + v.Set() + if !v.IsSet() { + t.Fatal("AtomicBool.Set() failed") + } + + v.UnSet() + if v.IsSet() { + t.Fatal("AtomicBool.UnSet() failed") + } +} + +func TestSetTo(t *testing.T) { + t.Parallel() + v := New() + + v.SetTo(true) + if !v.IsSet() { + t.Fatal("AtomicBool.SetTo(true) failed") + } + + v.SetTo(false) + if v.IsSet() { + t.Fatal("AtomicBool.SetTo(false) failed") + } + + if set := v.SetToIf(true, false); set || v.IsSet() { + t.Fatal("AtomicBool.SetTo(true, false) failed") + } + + if set := v.SetToIf(false, true); !set || !v.IsSet() { + t.Fatal("AtomicBool.SetTo(false, true) failed") + } +} + +func TestRace(t *testing.T) { + repeat := 10000 + var wg sync.WaitGroup + wg.Add(repeat * 3) + v := New() + + // Writer + go func() { + for i := 0; i < repeat; i++ { + v.Set() + wg.Done() + } + }() + + // Reader + go func() { + for i := 0; i < repeat; i++ { + v.IsSet() + wg.Done() + } + }() + + // Writer + go func() { + for i := 0; i < repeat; i++ { + v.UnSet() + wg.Done() + } + }() + wg.Wait() +} + +func TestJSONCompatibleWithBuiltinBool(t *testing.T) { + for _, value := range []bool{true, false} { + // Test bool -> bytes -> AtomicBool + + // act 1. bool -> bytes + buf, err := json.Marshal(value) + if err != nil { + t.Fatalf("json.Marshal(%t) failed: %s", value, err) + } + + // act 2. bytes -> AtomicBool + // + // Try to unmarshall the JSON byte slice + // of a normal boolean into an AtomicBool + // + // Create an AtomicBool with the oppsite default to ensure the unmarshal process did the work + ab := NewBool(!value) + err = json.Unmarshal(buf, ab) + if err != nil { + t.Fatalf(`json.Unmarshal("%s", %T) failed: %s`, buf, ab, err) + } + // assert + if ab.IsSet() != value { + t.Fatalf("Expected AtomicBool to represent %t but actual value was %t", value, ab.IsSet()) + } + + // Test AtomicBool -> bytes -> bool + + // act 3. AtomicBool -> bytes + buf, err = json.Marshal(ab) + if err != nil { + t.Fatalf("json.Marshal(%T) failed: %s", ab, err) + } + + // using the opposite value for the same reason as the former case + b := ab.IsNotSet() + // act 4. bytes -> bool + err = json.Unmarshal(buf, &b) + if err != nil { + t.Fatalf(`json.Unmarshal("%s", %T) failed: %s`, buf, &b, err) + } + // assert + if b != ab.IsSet() { + t.Fatalf(`json.Unmarshal("%s", %T) didn't work, expected %t, got %t`, buf, ab, ab.IsSet(), b) + } + } +} + +func TestUnmarshalJSONErrorNoWrite(t *testing.T) { + for _, val := range []bool{true, false} { + ab := NewBool(val) + oldVal := ab.IsSet() + buf := []byte("invalid-json") + err := json.Unmarshal(buf, ab) + if err == nil { + t.Fatalf(`Error expected from json.Unmarshal("%s", %T)`, buf, ab) + } + if oldVal != ab.IsSet() { + t.Fatal("Failed json.Unmarshal modified the value of AtomicBool which is not expected") + } + } +} + +func ExampleAtomicBool() { + cond := New() // default to false + any := true + old := any + new := !any + + cond.Set() // Sets to true + cond.IsSet() // Returns true + cond.UnSet() // Sets to false + cond.IsNotSet() // Returns true + cond.SetTo(any) // Sets to whatever you want + cond.SetToIf(new, old) // Sets to `new` only if the Boolean matches the `old`, returns whether succeeded +} + +// Benchmark Read + +func BenchmarkMutexRead(b *testing.B) { + var m sync.RWMutex + var v bool + b.ResetTimer() + for i := 0; i < b.N; i++ { + m.RLock() + _ = v + m.RUnlock() + } +} + +func BenchmarkAtomicValueRead(b *testing.B) { + var v atomic.Value + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = v.Load() != nil + } +} + +func BenchmarkAtomicBoolRead(b *testing.B) { + v := New() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = v.IsSet() + } +} + +// Benchmark Write + +func BenchmarkMutexWrite(b *testing.B) { + var m sync.RWMutex + var v bool + b.ResetTimer() + for i := 0; i < b.N; i++ { + m.RLock() + v = true + m.RUnlock() + } + b.StopTimer() + _ = v +} + +func BenchmarkAtomicValueWrite(b *testing.B) { + var v atomic.Value + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.Store(true) + } +} + +func BenchmarkAtomicBoolWrite(b *testing.B) { + v := New() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.Set() + } +} + +// Benchmark CAS + +func BenchmarkMutexCAS(b *testing.B) { + var m sync.RWMutex + var v bool + b.ResetTimer() + for i := 0; i < b.N; i++ { + m.Lock() + if !v { + v = true + } + m.Unlock() + } +} + +func BenchmarkAtomicBoolCAS(b *testing.B) { + v := New() + b.ResetTimer() + for i := 0; i < b.N; i++ { + v.SetToIf(false, true) + } +} diff --git a/v2/go.mod b/v2/go.mod new file mode 100644 index 0000000..53e2f65 --- /dev/null +++ b/v2/go.mod @@ -0,0 +1,3 @@ +module github.com/tevino/abool/v2 + +go 1.17