Help us understand the problem. What is going on with this article?

【Go】rand.Sourceを並列で使いまわすなんて何事だ

結論から言うと

rand.Sourceを使いまわすとpanicが起きます。
サーバーサイドで行うと、永遠に乱数パニックです。
しかも、本番環境にリリースするまでわからないため、うっかり実装すると凶悪バグの出来上がりです。

はじめに

インスタンスを再生成するコストを抑えるために使いまわそうと、誰でも一度は考えますよね。
スレッドセーフでどうかと言う概念を知らないと、サーバーをGoで立てたときに後悔することになります。
今回はrand.Sourceを使いまわすとどうなるのかを実験していきます。

実験

強制的にエラーを引き起こすために並列処理でrand.Sourceを呼ぶ実装を雑に作りました。

環境

$go version
go version go1.13 darwin/amd64

実験用ソースコード

main.go
package main

import (
    "fmt"
    "math/rand"
    "sync"
)

var randSource = NewRandSource()

// rand.Sourceを使いまわすとどうなるかの実験コード
func main() {

    ch := make(chan int, 100)
    wg := sync.WaitGroup{}

    for i := 0; i < 1000; i++ {
        ch <- 1
        wg.Add(1)
        go func() {
            calcRnad()
            <-ch
            wg.Done()
        }()
        fmt.Println("for count =", i)
    }
    wg.Wait()
}

func calcRnad() {
    for i := 0; i < 10000; i++ {
        randSource.Intn(1000) // インスタンスを使いまわす場合
        //      NewRandSource().Intn(1000)// インスタンスを毎回生成する記述
    }
}

// NewRandSource 新しいインスタンスを作る
func NewRandSource() *rand.Rand {
    return rand.New(rand.NewSource(time.Now().UnixNano()))
}

簡単に説明すると100のスレッドを作って、10000回randを呼び出す処理を書いただけです。
動作チェックのためにエラーにならないコードをコメントしています。

実行結果

下記の通り12スレッド目で、panic:runtimeエラーが起きました。

go run main.go 
for count = 0
for count = 1
for count = 2
for count = 3
for count = 4
for count = 5
for count = 6
for count = 7
for count = 8
for count = 9
for count = 10
for count = 11
for count = 12
panic: runtime error: index out of range [-1]

goroutine 22 [running]:
math/rand.(*rngSource).Uint64(...)
    /usr/local/Cellar/go/1.13/libexec/src/math/rand/rng.go:249
math/rand.(*rngSource).Int63(0xc000089500, 0xf886e3da440ba38)
    /usr/local/Cellar/go/1.13/libexec/src/math/rand/rng.go:234 +0x93
math/rand.(*Rand).Int63(...)
    /usr/local/Cellar/go/1.13/libexec/src/math/rand/rand.go:85
math/rand.(*Rand).Int31(...)
    /usr/local/Cellar/go/1.13/libexec/src/math/rand/rand.go:99
math/rand.(*Rand).Int31n(0xc0000641b0, 0x3e8, 0x165)
    /usr/local/Cellar/go/1.13/libexec/src/math/rand/rand.go:134 +0x5f
math/rand.(*Rand).Intn(0xc0000641b0, 0x3e8, 0x165)
    /usr/local/Cellar/go/1.13/libexec/src/math/rand/rand.go:172 +0x45
main.calcRnad()
    /playground/go/src/rand/main.go:32 +0x3f
main.main.func1(0xc00008c000, 0xc000016054)
    /playground/go/src/rand/main.go:21 +0x22
created by main.main
    /playground/go/src/rand/main.go:20 +0xc7
exit status 2

painicとなった箇所の実装を眺めてみる

対象ファイルの237行目から

math/rand/rng.go
// Uint64 returns a non-negative pseudo-random 64-bit integer as an uint64.
func (rng *rngSource) Uint64() uint64 {
    rng.tap--
    if rng.tap < 0 {
        rng.tap += rngLen
    }

    rng.feed--
    if rng.feed < 0 {
        rng.feed += rngLen
    }

    x := rng.vec[rng.feed] + rng.vec[rng.tap]
    rng.vec[rng.feed] = x
    return uint64(x)
}

rng.tapとrng.feedが負の値にならないように実装しているのがわかります。

しかし、並列処理の場合は
1. スレッドA処理:rng.tap < 0のチェック
2. スレッドB処理:rng.tap--で負の値になる
3. スレッドA処理:rng.vec[rng.tap]が負のindexを見てpanicになる
となる可能性が確率的に存在します

おわりに

並列で使わなければいいんです。並列で。

参考

なおissuesでも議論されてる模様
https://github.com/golang/go/issues/21393

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away