diff --git a/bool.go b/bool.go index bf1eb86..87256b7 100644 --- a/bool.go +++ b/bool.go @@ -24,21 +24,23 @@ func NewBool(ok bool) *AtomicBool { // 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 +type AtomicBool struct { + boolean int32 +} // Set sets the Boolean to true. func (ab *AtomicBool) Set() { - atomic.StoreInt32((*int32)(ab), 1) + atomic.StoreInt32(&ab.boolean, 1) } // UnSet sets the Boolean to false. func (ab *AtomicBool) UnSet() { - atomic.StoreInt32((*int32)(ab), 0) + atomic.StoreInt32(&ab.boolean, 0) } // IsSet returns whether the Boolean is true. func (ab *AtomicBool) IsSet() bool { - return atomic.LoadInt32((*int32)(ab)) == 1 + return atomic.LoadInt32(&ab.boolean)&1 == 1 } // IsNotSet returns whether the Boolean is false. @@ -49,9 +51,21 @@ func (ab *AtomicBool) IsNotSet() bool { // SetTo sets the boolean with given Boolean. func (ab *AtomicBool) SetTo(yes bool) { if yes { - atomic.StoreInt32((*int32)(ab), 1) + atomic.StoreInt32(&ab.boolean, 1) } else { - atomic.StoreInt32((*int32)(ab), 0) + atomic.StoreInt32(&ab.boolean, 0) + } +} + +// Toggle inverts the Boolean then returns the value before inverting. +// Based on: https://github.com/uber-go/atomic/blob/3504dfaa1fa414923b1c8693f45d2f6931daf229/bool_ext.go#L40 +func (ab *AtomicBool) Toggle() bool { + var old bool + for { + old = ab.IsSet() + if ab.SetToIf(old, !old) { + return old + } } } @@ -65,7 +79,7 @@ func (ab *AtomicBool) SetToIf(old, new bool) (set bool) { if new { n = 1 } - return atomic.CompareAndSwapInt32((*int32)(ab), o, n) + return atomic.CompareAndSwapInt32(&ab.boolean, o, n) } // MarshalJSON behaves the same as if the AtomicBool is a builtin.bool. diff --git a/bool_test.go b/bool_test.go index d418c48..2a977dd 100644 --- a/bool_test.go +++ b/bool_test.go @@ -72,6 +72,43 @@ func TestSetTo(t *testing.T) { } } +func TestToggle(t *testing.T) { + t.Parallel() + v := New() + + _ = v.Toggle() + if !v.IsSet() { + t.Fatal("AtomicBool.Toggle() to true failed") + } + + prev := v.Toggle() + if v.IsSet() == prev { + t.Fatal("AtomicBool.Toggle() to false failed") + } +} + +func TestToogleMultipleTimes(t *testing.T) { + t.Parallel() + + v := New() + pre := !v.IsSet() + for i := 0; i < 100; i++ { + v.SetTo(false) + for j := 0; j < i; j++ { + pre = v.Toggle() + } + + expected := i%2 != 0 + if v.IsSet() != expected { + t.Fatalf("AtomicBool.Toogle() doesn't work after %d calls, expected: %v, got %v", i, expected, v.IsSet()) + } + + if pre == v.IsSet() { + t.Fatalf("AtomicBool.Toogle() returned wrong value at the %dth calls, expected: %v, got %v", i, !v.IsSet(), pre) + } + } +} + func TestRace(t *testing.T) { repeat := 10000 var wg sync.WaitGroup @@ -104,6 +141,22 @@ func TestRace(t *testing.T) { wg.Wait() } +func TestSetToIfAfterMultipleToggles(t *testing.T) { + v := New() // false + + v.Toggle() // true + v.Toggle() // false + v.Toggle() // true + + // As v is true, it should now be flipped to false + v.SetToIf(true, false) + expected := false + + if v.IsSet() != expected { + t.Fatalf("Toggling the value atleast 3 times, until it's true, `SetToIf(true, false)` should flip v to false, expected: %v, got %v", expected, v.IsSet()) + } +} + func TestJSONCompatibleWithBuiltinBool(t *testing.T) { for _, value := range []bool{true, false} { // Test bool -> bytes -> AtomicBool @@ -181,6 +234,14 @@ func ExampleAtomicBool() { cond.SetToIf(new, old) // Sets to `new` only if the Boolean matches the `old`, returns whether succeeded } +func BenchmarkAtomicBoolToggle(b *testing.B) { + v := New() + b.ResetTimer() + for i := 0; i < b.N; i++ { + _ = v.Toggle() + } +} + // Benchmark Read func BenchmarkMutexRead(b *testing.B) {