5
2

More than 3 years have passed since last update.

Goでプリミティブなアトミック操作をするならuber-go/atomicを使ってほしい話と、比較不能かつコスト0な型の話

Last updated at Posted at 2021-09-16

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

だが以前のバージョンでもそれほど変わっていないものと思われる。

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2