Skip to content

Commit

Permalink
Add CompareAndSwap and Swap, Deprecate CAS (#111)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
eNV25 authored Aug 6, 2022
1 parent 01497d2 commit d4bbbc8
Show file tree
Hide file tree
Showing 18 changed files with 201 additions and 40 deletions.
9 changes: 8 additions & 1 deletion bool.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion duration.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
11 changes: 11 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
}
4 changes: 2 additions & 2 deletions error_ext.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 }

Expand Down
28 changes: 28 additions & 0 deletions error_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}
23 changes: 15 additions & 8 deletions float32_ext.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand Down
23 changes: 15 additions & 8 deletions float64_ext.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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.
Expand Down
7 changes: 7 additions & 0 deletions int32.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
7 changes: 7 additions & 0 deletions int64.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
9 changes: 8 additions & 1 deletion internal/gen-atomicint/main.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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)
}
Expand Down
44 changes: 30 additions & 14 deletions internal/gen-atomicwrapper/main.go
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -271,16 +270,33 @@ 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 }}
{{ if .Swap -}}
// 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 }}
Expand Down
11 changes: 11 additions & 0 deletions string.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
6 changes: 2 additions & 4 deletions string_ext.go
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 {
Expand Down
20 changes: 20 additions & 0 deletions string_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
}
7 changes: 7 additions & 0 deletions uint32.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}

Expand Down
Loading

0 comments on commit d4bbbc8

Please sign in to comment.