atomicを直接使う人はそう多くないと思われるが、仮に使うようなことがあった場合は全人類標準ライブラリのsync/atomicではなくuber-go/atomicを使うべきである。
なぜsync/atomicはよくないのか
以下のようなコードにおいて、sync/atomic
は型による保護機構が欠如しているために誤った操作に気がつくことができない。
package main
import (
"fmt"
"sync/atomic"
)
type Money struct {
price int32
}
func main() {
ended := make(chan int)
mine := &Money{price: 1}
go func() {
// atomic.AddInt32(&mine.price, 1) とすべきところで間違いに気が付かない
mine.price += 1
ended <- 0
}()
// atomic.AddInt32(&mine.price, 1) とすべきところで間違いに気が付かない
mine.price += 1
_ = <- ended
fmt.Println(mine)
}
せっかく静的な型システムがあるのにこれは残念すぎる。
これは uber-go/atomic
を使うことで対処可能である。
こちらで定義されている型を使った場合、以下のようにアトミックな操作でないコンパイル時にはじくことができる。
package main
import (
"fmt"
"go.uber.org/atomic"
)
type Money struct {
price *atomic.Int32
}
func main() {
ended := make(chan int)
x := atomic.NewInt32(1)
mine := &Money{price: x}
go func() {
// mine.price += 1
// > コンパイルエラー
// > invalid operation: mine.price += 1 (mismatched types *"go.uber.org/atomic".Int32 and int)
mine.price.Add(1)
ended <- 0
}()
// mine.price += 1
// > コンパイルエラー
// > invalid operation: mine.price += 1 (mismatched types *"go.uber.org/atomic".Int32 and int)
mine.price.Add(1)
_ = <- ended
fmt.Println(mine.price.Load())
}
またすべての非アトミック操作ができなくなるわけではないが(dereferenceはできる)、以下のように比較しようとした場合にコンパイルエラーになる。
x := atomic.NewInt32(1)
y := atomic.NewInt32(1)
fmt.Println(*x == *y) // <-- invalid operation: *xx == *yy (struct containing "go.uber.org/atomic".nocmp cannot be compared)
もちろんLoad()
を使えばこれは解決できる。
比較不能な型
nocmpという型を定義してそれを使っている。
コードを抜粋すると以下の箇所。
// nocmp is an uncomparable struct. Embed this inside another struct to make
// it uncomparable.
//
// type Foo struct {
// nocmp
// // ...
// }
//
// This DOES NOT:
//
// - Disallow shallow copies of structs
// - Disallow comparison of pointers to uncomparable structs
type nocmp [0]func()
ここでnocmpの定義は[0]func()
となっており、これをフィールドとして持っている構造体は比較不能となっている。
これはgo4org/memからもってきたそうで、こちらの定義のコメントによると
// (...). Its various methods should inline & compile to the equivalent operations
// working on a string or []byte directly.
とのことでsync/atomic
に比べて余計なコストがかかるというパフォーマンス上の心配はしなくてもよさそうだ。
なぜこの形で定義されているのかまでは調べきれなかったが、func型が比較不能型である点と上記のコメントのようにサイズ0の配列は最適化で消え去ることで0コストになるあたりを組み合わせた結果このような形になったものと思われる。
このあたりのロジックはGo本体の
- src/cmd/compile/internal/typecheck/expr.go のtoArith関数内
- src/cmd/compile/internal/types/alg.go の AlgType関数内
にある。調べた時点でのGoのバージョン(gitのrevisionだが)は
$ git rev-parse HEAD
cfa233d76bcff00f46f5e5acdb17cb819a309d2b
だが以前のバージョンでもそれほど変わっていないものと思われる。