From d4bbbc828dd5bdb865c6c0effe4178f7ef505eb0 Mon Sep 17 00:00:00 2001 From: eNV25 Date: Sat, 6 Aug 2022 22:12:19 +0400 Subject: [PATCH] Add CompareAndSwap and Swap, Deprecate CAS (#111) Adds CompareAndSwap and Swap methods to String, Error, and Value, implemented by making use of Value.CompareAndSwap and Value.Swap added in Go 1.17. Following that, add CompareAndSwap to all other types with "CAS" methods and deprecate CAS in favor of CompareAndSwap, since that's the convention the standard library chose for these in Go 1.19. --- bool.go | 9 +++++- duration.go | 9 +++++- error.go | 11 ++++++++ error_ext.go | 4 +-- error_test.go | 28 +++++++++++++++++++ float32_ext.go | 23 ++++++++++------ float64_ext.go | 23 ++++++++++------ int32.go | 7 +++++ int64.go | 7 +++++ internal/gen-atomicint/main.go | 9 +++++- internal/gen-atomicwrapper/main.go | 44 ++++++++++++++++++++---------- string.go | 11 ++++++++ string_ext.go | 6 ++-- string_test.go | 20 ++++++++++++++ uint32.go | 7 +++++ uint64.go | 7 +++++ uintptr.go | 7 +++++ unsafe_pointer.go | 9 +++++- 18 files changed, 201 insertions(+), 40 deletions(-) diff --git a/bool.go b/bool.go index caa373e..dfa2085 100644 --- a/bool.go +++ b/bool.go @@ -55,8 +55,15 @@ func (x *Bool) Store(val bool) { } // CAS is an atomic compare-and-swap for bool values. +// +// Deprecated: Use CompareAndSwap. func (x *Bool) CAS(old, new bool) (swapped bool) { - return x.v.CAS(boolToInt(old), boolToInt(new)) + return x.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap for bool values. +func (x *Bool) CompareAndSwap(old, new bool) (swapped bool) { + return x.v.CompareAndSwap(boolToInt(old), boolToInt(new)) } // Swap atomically stores the given bool and returns the old diff --git a/duration.go b/duration.go index 113f8fb..6f41574 100644 --- a/duration.go +++ b/duration.go @@ -56,8 +56,15 @@ func (x *Duration) Store(val time.Duration) { } // CAS is an atomic compare-and-swap for time.Duration values. +// +// Deprecated: Use CompareAndSwap. func (x *Duration) CAS(old, new time.Duration) (swapped bool) { - return x.v.CAS(int64(old), int64(new)) + return x.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap for time.Duration values. +func (x *Duration) CompareAndSwap(old, new time.Duration) (swapped bool) { + return x.v.CompareAndSwap(int64(old), int64(new)) } // Swap atomically stores the given time.Duration and returns the old diff --git a/error.go b/error.go index 2af1ef6..27b23ea 100644 --- a/error.go +++ b/error.go @@ -49,3 +49,14 @@ func (x *Error) Load() error { func (x *Error) Store(val error) { x.v.Store(packError(val)) } + +// CompareAndSwap is an atomic compare-and-swap for error values. +func (x *Error) CompareAndSwap(old, new error) (swapped bool) { + return x.v.CompareAndSwap(packError(old), packError(new)) +} + +// Swap atomically stores the given error and returns the old +// value. +func (x *Error) Swap(val error) (old error) { + return unpackError(x.v.Swap(packError(val))) +} diff --git a/error_ext.go b/error_ext.go index ffe0be2..d31fb63 100644 --- a/error_ext.go +++ b/error_ext.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020 Uber Technologies, Inc. +// Copyright (c) 2020-2022 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -23,7 +23,7 @@ package atomic // atomic.Value panics on nil inputs, or if the underlying type changes. // Stabilize by always storing a custom struct that we control. -//go:generate bin/gen-atomicwrapper -name=Error -type=error -wrapped=Value -pack=packError -unpack=unpackError -file=error.go +//go:generate bin/gen-atomicwrapper -name=Error -type=error -wrapped=Value -pack=packError -unpack=unpackError -compareandswap -swap -file=error.go type packedError struct{ Value error } diff --git a/error_test.go b/error_test.go index cab2e7d..c9ac2ef 100644 --- a/error_test.go +++ b/error_test.go @@ -53,3 +53,31 @@ func TestNewErrorWithError(t *testing.T) { atom.Store(err2) require.Equal(t, err2, atom.Load(), "Expected Load to return overridden value") } + +func TestErrorSwap(t *testing.T) { + err1 := errors.New("hello1") + err2 := errors.New("hello2") + + atom := NewError(err1) + require.Equal(t, err1, atom.Load(), "Expected Load to return initialized value") + + old := atom.Swap(err2) + require.Equal(t, err2, atom.Load(), "Expected Load to return overridden value") + require.Equal(t, err1, old, "Expected old to be initial value") +} + +func TestErrorCompareAndSwap(t *testing.T) { + err1 := errors.New("hello1") + err2 := errors.New("hello2") + + atom := NewError(err1) + require.Equal(t, err1, atom.Load(), "Expected Load to return initialized value") + + swapped := atom.CompareAndSwap(err2, err2) + require.False(t, swapped, "Expected swapped to be false") + require.Equal(t, err1, atom.Load(), "Expected Load to return initial value") + + swapped = atom.CompareAndSwap(err1, err2) + require.True(t, swapped, "Expected swapped to be true") + require.Equal(t, err2, atom.Load(), "Expected Load to return overridden value") +} diff --git a/float32_ext.go b/float32_ext.go index 39034d2..b0cd8d9 100644 --- a/float32_ext.go +++ b/float32_ext.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020 Uber Technologies, Inc. +// Copyright (c) 2020-2022 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -45,21 +45,28 @@ func (f *Float32) Sub(delta float32) float32 { // CAS is an atomic compare-and-swap for float32 values. // -// Note: CAS handles NaN incorrectly. NaN != NaN using Go's inbuilt operators -// but CAS allows a stored NaN to compare equal to a passed in NaN. -// This avoids typical CAS loops from blocking forever, e.g., +// Deprecated: Use CompareAndSwap +func (f *Float32) CAS(old, new float32) (swapped bool) { + return f.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap for float32 values. +// +// Note: CompareAndSwap handles NaN incorrectly. NaN != NaN using Go's inbuilt operators +// but CompareAndSwap allows a stored NaN to compare equal to a passed in NaN. +// This avoids typical CompareAndSwap loops from blocking forever, e.g., // // for { // old := atom.Load() // new = f(old) -// if atom.CAS(old, new) { +// if atom.CompareAndSwap(old, new) { // break // } // } // -// If CAS did not match NaN to match, then the above would loop forever. -func (f *Float32) CAS(old, new float32) (swapped bool) { - return f.v.CAS(math.Float32bits(old), math.Float32bits(new)) +// If CompareAndSwap did not match NaN to match, then the above would loop forever. +func (f *Float32) CompareAndSwap(old, new float32) (swapped bool) { + return f.v.CompareAndSwap(math.Float32bits(old), math.Float32bits(new)) } // String encodes the wrapped value as a string. diff --git a/float64_ext.go b/float64_ext.go index 0bcaaed..48c52b0 100644 --- a/float64_ext.go +++ b/float64_ext.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020 Uber Technologies, Inc. +// Copyright (c) 2020-2022 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -45,21 +45,28 @@ func (f *Float64) Sub(delta float64) float64 { // CAS is an atomic compare-and-swap for float64 values. // -// Note: CAS handles NaN incorrectly. NaN != NaN using Go's inbuilt operators -// but CAS allows a stored NaN to compare equal to a passed in NaN. -// This avoids typical CAS loops from blocking forever, e.g., +// Deprecated: Use CompareAndSwap +func (f *Float64) CAS(old, new float64) (swapped bool) { + return f.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap for float64 values. +// +// Note: CompareAndSwap handles NaN incorrectly. NaN != NaN using Go's inbuilt operators +// but CompareAndSwap allows a stored NaN to compare equal to a passed in NaN. +// This avoids typical CompareAndSwap loops from blocking forever, e.g., // // for { // old := atom.Load() // new = f(old) -// if atom.CAS(old, new) { +// if atom.CompareAndSwap(old, new) { // break // } // } // -// If CAS did not match NaN to match, then the above would loop forever. -func (f *Float64) CAS(old, new float64) (swapped bool) { - return f.v.CAS(math.Float64bits(old), math.Float64bits(new)) +// If CompareAndSwap did not match NaN to match, then the above would loop forever. +func (f *Float64) CompareAndSwap(old, new float64) (swapped bool) { + return f.v.CompareAndSwap(math.Float64bits(old), math.Float64bits(new)) } // String encodes the wrapped value as a string. diff --git a/int32.go b/int32.go index b513810..b9a68f4 100644 --- a/int32.go +++ b/int32.go @@ -66,7 +66,14 @@ func (i *Int32) Dec() int32 { } // CAS is an atomic compare-and-swap. +// +// Deprecated: Use CompareAndSwap. func (i *Int32) CAS(old, new int32) (swapped bool) { + return i.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap. +func (i *Int32) CompareAndSwap(old, new int32) (swapped bool) { return atomic.CompareAndSwapInt32(&i.v, old, new) } diff --git a/int64.go b/int64.go index e11a568..78d2609 100644 --- a/int64.go +++ b/int64.go @@ -66,7 +66,14 @@ func (i *Int64) Dec() int64 { } // CAS is an atomic compare-and-swap. +// +// Deprecated: Use CompareAndSwap. func (i *Int64) CAS(old, new int64) (swapped bool) { + return i.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap. +func (i *Int64) CompareAndSwap(old, new int64) (swapped bool) { return atomic.CompareAndSwapInt64(&i.v, old, new) } diff --git a/internal/gen-atomicint/main.go b/internal/gen-atomicint/main.go index b00a208..6132691 100644 --- a/internal/gen-atomicint/main.go +++ b/internal/gen-atomicint/main.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020 Uber Technologies, Inc. +// Copyright (c) 2020-2022 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -180,7 +180,14 @@ func (i *{{ .Name }}) Dec() {{ .Wrapped }} { } // CAS is an atomic compare-and-swap. +// +// Deprecated: Use CompareAndSwap. func (i *{{ .Name }}) CAS(old, new {{ .Wrapped }}) (swapped bool) { + return i.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap. +func (i *{{ .Name }}) CompareAndSwap(old, new {{ .Wrapped }}) (swapped bool) { return atomic.CompareAndSwap{{ .Name }}(&i.v, old, new) } diff --git a/internal/gen-atomicwrapper/main.go b/internal/gen-atomicwrapper/main.go index 1dd056d..b85501c 100644 --- a/internal/gen-atomicwrapper/main.go +++ b/internal/gen-atomicwrapper/main.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020 Uber Technologies, Inc. +// Copyright (c) 2020-2022 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -96,9 +96,10 @@ func run(args []string) error { Imports stringList Pack, Unpack string - CAS bool - Swap bool - JSON bool + CAS bool + CompareAndSwap bool + Swap bool + JSON bool File string ToYear int @@ -129,7 +130,9 @@ func run(args []string) error { // Switches for individual methods. Underlying atomics must support // these. flag.BoolVar(&opts.CAS, "cas", false, - "generate a `CAS(old, new) bool` method; requires -pack") + "generate a deprecated `CAS(old, new) bool` method; requires -pack") + flag.BoolVar(&opts.CompareAndSwap, "compareandswap", false, + "generate a `CompareAndSwap(old, new) bool` method; requires -pack") flag.BoolVar(&opts.Swap, "swap", false, "generate a `Swap(new) old` method; requires -pack and -unpack") flag.BoolVar(&opts.JSON, "json", false, @@ -147,12 +150,8 @@ func run(args []string) error { return errors.New("either both, or neither of -pack and -unpack must be specified") } - if opts.CAS && len(opts.Pack) == 0 { - return errors.New("flag -cas requires -pack") - } - - if opts.Swap && len(opts.Pack) == 0 { - return errors.New("flag -swap requires -pack and -unpack") + if opts.CAS { + opts.CompareAndSwap = true } var w io.Writer = os.Stdout @@ -240,7 +239,7 @@ var _zero{{ .Name }} {{ .Type }} // New{{ .Name }} creates a new {{ .Name }}. -func New{{ .Name}}(val {{ .Type }}) *{{ .Name }} { +func New{{ .Name }}(val {{ .Type }}) *{{ .Name }} { x := &{{ .Name }}{} if val != _zero{{ .Name }} { x.Store(val) @@ -271,8 +270,21 @@ func (x *{{ .Name }}) Store(val {{ .Type }}) { {{ if .CAS -}} // CAS is an atomic compare-and-swap for {{ .Type }} values. + // + // Deprecated: Use CompareAndSwap. func (x *{{ .Name }}) CAS(old, new {{ .Type }}) (swapped bool) { - return x.v.CAS({{ .Pack }}(old), {{ .Pack }}(new)) + return x.CompareAndSwap(old, new) + } +{{- end }} + +{{ if .CompareAndSwap -}} + // CompareAndSwap is an atomic compare-and-swap for {{ .Type }} values. + func (x *{{ .Name }}) CompareAndSwap(old, new {{ .Type }}) (swapped bool) { + {{ if .Pack -}} + return x.v.CompareAndSwap({{ .Pack }}(old), {{ .Pack }}(new)) + {{- else -}}{{- /* assume go.uber.org/atomic.Value */ -}} + return x.v.CompareAndSwap(old, new) + {{- end }} } {{- end }} @@ -280,7 +292,11 @@ func (x *{{ .Name }}) Store(val {{ .Type }}) { // Swap atomically stores the given {{ .Type }} and returns the old // value. func (x *{{ .Name }}) Swap(val {{ .Type }}) (old {{ .Type }}) { - return {{ .Unpack }}(x.v.Swap({{ .Pack }}(val))) + {{ if .Pack -}} + return {{ .Unpack }}(x.v.Swap({{ .Pack }}(val))) + {{- else -}}{{- /* assume go.uber.org/atomic.Value */ -}} + return x.v.Swap(val).({{ .Type }}) + {{- end }} } {{- end }} diff --git a/string.go b/string.go index 46a89dc..c4bea70 100644 --- a/string.go +++ b/string.go @@ -52,3 +52,14 @@ func (x *String) Load() string { func (x *String) Store(val string) { x.v.Store(val) } + +// CompareAndSwap is an atomic compare-and-swap for string values. +func (x *String) CompareAndSwap(old, new string) (swapped bool) { + return x.v.CompareAndSwap(old, new) +} + +// Swap atomically stores the given string and returns the old +// value. +func (x *String) Swap(val string) (old string) { + return x.v.Swap(val).(string) +} diff --git a/string_ext.go b/string_ext.go index 83d92ed..1f63dfd 100644 --- a/string_ext.go +++ b/string_ext.go @@ -1,4 +1,4 @@ -// Copyright (c) 2020 Uber Technologies, Inc. +// Copyright (c) 2020-2022 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -20,9 +20,7 @@ package atomic -//go:generate bin/gen-atomicwrapper -name=String -type=string -wrapped=Value -file=string.go -// Note: No Swap as String wraps Value, which wraps the stdlib sync/atomic.Value which -// only supports Swap as of go1.17: https://github.com/golang/go/issues/39351 +//go:generate bin/gen-atomicwrapper -name=String -type=string -wrapped=Value -compareandswap -swap -file=string.go // String returns the wrapped value. func (s *String) String() string { diff --git a/string_test.go b/string_test.go index a730f5f..54121d1 100644 --- a/string_test.go +++ b/string_test.go @@ -82,4 +82,24 @@ func TestString(t *testing.T) { assert.Equal(t, "foo", atom.String(), "String() returned an unexpected value.") }) + + t.Run("CompareAndSwap", func(t *testing.T) { + atom := NewString("foo") + + swapped := atom.CompareAndSwap("bar", "bar") + require.False(t, swapped, "swapped isn't false") + require.Equal(t, atom.Load(), "foo", "Load returned wrong value") + + swapped = atom.CompareAndSwap("foo", "bar") + require.True(t, swapped, "swapped isn't true") + require.Equal(t, atom.Load(), "bar", "Load returned wrong value") + }) + + t.Run("Swap", func(t *testing.T) { + atom := NewString("foo") + + old := atom.Swap("bar") + require.Equal(t, old, "foo", "Swap returned wrong value") + require.Equal(t, atom.Load(), "bar", "Load returned wrong value") + }) } diff --git a/uint32.go b/uint32.go index 60e832d..d6f04a9 100644 --- a/uint32.go +++ b/uint32.go @@ -66,7 +66,14 @@ func (i *Uint32) Dec() uint32 { } // CAS is an atomic compare-and-swap. +// +// Deprecated: Use CompareAndSwap. func (i *Uint32) CAS(old, new uint32) (swapped bool) { + return i.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap. +func (i *Uint32) CompareAndSwap(old, new uint32) (swapped bool) { return atomic.CompareAndSwapUint32(&i.v, old, new) } diff --git a/uint64.go b/uint64.go index 115b3db..2574bdd 100644 --- a/uint64.go +++ b/uint64.go @@ -66,7 +66,14 @@ func (i *Uint64) Dec() uint64 { } // CAS is an atomic compare-and-swap. +// +// Deprecated: Use CompareAndSwap. func (i *Uint64) CAS(old, new uint64) (swapped bool) { + return i.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap. +func (i *Uint64) CompareAndSwap(old, new uint64) (swapped bool) { return atomic.CompareAndSwapUint64(&i.v, old, new) } diff --git a/uintptr.go b/uintptr.go index e4a361c..81b275a 100644 --- a/uintptr.go +++ b/uintptr.go @@ -66,7 +66,14 @@ func (i *Uintptr) Dec() uintptr { } // CAS is an atomic compare-and-swap. +// +// Deprecated: Use CompareAndSwap. func (i *Uintptr) CAS(old, new uintptr) (swapped bool) { + return i.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap. +func (i *Uintptr) CompareAndSwap(old, new uintptr) (swapped bool) { return atomic.CompareAndSwapUintptr(&i.v, old, new) } diff --git a/unsafe_pointer.go b/unsafe_pointer.go index 169f793..34868ba 100644 --- a/unsafe_pointer.go +++ b/unsafe_pointer.go @@ -1,4 +1,4 @@ -// Copyright (c) 2021 Uber Technologies, Inc. +// Copyright (c) 2021-2022 Uber Technologies, Inc. // // Permission is hereby granted, free of charge, to any person obtaining a copy // of this software and associated documentation files (the "Software"), to deal @@ -53,6 +53,13 @@ func (p *UnsafePointer) Swap(val unsafe.Pointer) (old unsafe.Pointer) { } // CAS is an atomic compare-and-swap. +// +// Deprecated: Use CompareAndSwap func (p *UnsafePointer) CAS(old, new unsafe.Pointer) (swapped bool) { + return p.CompareAndSwap(old, new) +} + +// CompareAndSwap is an atomic compare-and-swap. +func (p *UnsafePointer) CompareAndSwap(old, new unsafe.Pointer) (swapped bool) { return atomic.CompareAndSwapPointer(&p.v, old, new) }