読者です 読者をやめる 読者になる 読者になる

DRYな備忘録

Don't Repeat Yourself.

【Go言語】ループ内でのgoroutine生成の潜在的なエンバグポイントについて

go

1番、よくやるやつ(やばめ)

   for _, user := range users {
        go func() {
            user.SomeHeavy()
        }()
    }

非同期で生成されるクロージャの中で、参照されるuserはループ内スコープのそれなので、たとえばループのn番目とn+1番目の処理において、同じn+1番目のユーザに重複してSomeHeavy()を実行してしまう危険性がある。ぜんぜん閉包じゃない。今回の主題は、この潜在的なエンバグポイントを、どうすれば事前に防げるか、防げるコーディング規約があるか、みたいな興味。

2番、よくやるやつ(だいたい)

   for _, user := range users {
        go func(u *User) {
            u.SomeHeavy()
        }(user)
    }

ちゃんとクロージャ内でのスコープとなるように渡す。これなら1番の問題は起きない。

そもそも、1番のように書けなければ問題無いんだけど、コンパイルは普通に通るし、書いてもgo vetが叱ってくれないような気がする(叱ってくれる?)。

3番、なるべく名前のある関数にすればいいんではないか?

    doHeavyToUser := func(u *User) {
        u.SomeHeavy()
    }
    for _, user := range users {
        go doHeavyToUser(user)
    }

これで1番のときみたいな問題はなるべく起きないようになった気がする

でも、ループでgoroutineつくるとき、ほとんどの場合「たくさん作ったgoroutineが全員帰ってくるまで待つ」みたいなことしたいので、以下4番のように書くことが多い

4番、sync.WaitGroupつかう(主題とはかんけいなく、よくやるよね、っていうコード)

   var g sync.WaitGroup
    for _, user := range users {
        // カウンタ増やす
        g.Add(1)
        go func(u *User) {
            u.SomeHeavy()
            // カウンタ減らす
            g.Done()
        }(user)
    }
    // カウンタがゼロになるまで待つ
    g.Wait()
 
    fmt.Println("おわったー")

これも含めて、3番のように書こうと思ったら、以下5番のように書かざるをえない?

5番、抜き出した名前付き関数にWaitGroupも渡してしまう

    doHeavyToUser := func(u *User, wg sync.WaitGroup) {
        u.SomeHeavy()
        wg.Done()
    }

    var g sync.WaitGroup
    for _, user := range users {
        g.Add(1)
        doHeavyToUser(user, g)
    }
    g.Wait()

    fmt.Println("おわったー")

逆に見通し悪くないか。なお、doHeavyToUserをループ生成しているメソッドの外で定義すると、逆に以下のように叱られる

 doHeavyToUser passes Lock by value: sync.WaitGroup contains sync.Mutex

「doHeavyToUser関数は処理をLockしちゃうかもよ」というgo vetの親切心。

現実的には4番がいちばん分かりやすいのかな。uの部分をuserと書かないように気をつけるしかないのかな...

DRY