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

続、workerパターンをcontext化してみたら……

みなさん「みんなのGo言語」は予約ポチりましたか? 私はポチりました!

そんな「みんなのGo言語」の著者の一人であるid:lestrrat さんからの前エントリに対してマサカリが飛んできてます。

workerパターンをcontext化してみたら…… - okzkメモ

context.Contextを揮発性のないものでラップして持つのはよくないと思う。このほうがよりGoっぽいと思うが、どうだろう https://gist.github.com/lestrrat/c9b78369cf9b9c5d9b0c909ed1e2452e

2016/08/23 18:34

コメント内のgistのリンクはこちら

ちょっと時間あいちゃいましたけど、これについて思ったことを2点ほど……

まずはcontext関係ないとこから……

contextの使い方へのツッコミなのにいきなりcontextとは無関係なトコに言及しちゃうのですが、同時実行数の制御の仕方が前回までのコードと異なってるのにモヤモヤしちゃいました。

前回までのコードの方は以下のようなカンジ(以下「worker方式)」で

  1. 同時実行数分のworkerのgoroutineを立ち上げる
  2. その中で、ループでchannel経由で回ってきたjobを処理する

んで、頂いたgistの方は、こんなカンジ(以下「セマフォ方式)」

  1. job分goroutineを立ち上げておいて
  2. セマフォで同時実行数を制限する

元々の「goroutineを爆発させないため」という記事の流れもあったんでworker方式だったというのもあるんですけど、 実際どちらの実装方式も選べるなら、個人的には以下のような理由でやっぱりworker方式を選ぶような気がします。

  • なんとなくセマフォ方式よりworker方式の方が可読性が高い気がする
  • goroutineのそのもののコスト(どんなに軽量でもゼロではない)

可読性は個人の主観によるのでどうしても宗教論争になっちゃいますけど、後者の実行コストの方を検証してみましょう。

というわけで、簡易なベンチマークを用意してみました。

package main

import (
  "sync"
  "testing"
  "time"
)

const concurrency = 4

func BenchmarkLimitByWorkers(b *testing.B) {
  ch := make(chan struct{}, 10000)

  wg := sync.WaitGroup{}
  wg.Add(concurrency)
  for i := 0; i < concurrency; i++ {
    go func() {
      for range ch {
        // 何かしら処理の代わり
        // ベンチマークそのものに支配的にならないようにマイクロ秒だけsleep
        time.Sleep(time.Microsecond)
      }
      wg.Done()
    }()
  }

  for i := 0; i < b.N; i++ {
    ch <- struct{}{}
  }
  close(ch)
  wg.Wait()
}

func BenchmarkLimitBySemaphore(b *testing.B) {
  ch := make(chan struct{}, concurrency)

  wg := sync.WaitGroup{}
  wg.Add(b.N)
  for i := 0; i < b.N; i++ {
    go func() {
      ch <- struct{}{}
      // 何かしら処理の代わり
      // ベンチマークそのものに支配的にならないようにマイクロ秒だけsleep
      time.Sleep(time.Microsecond)
      <-ch
      wg.Done()
    }()
  }
  wg.Wait()
}

手元で実行してみた結果は以下のようなカンジ。

BenchmarkLimitByWorkers-4         300000              4872 ns/op
BenchmarkLimitBySemaphore-4       200000             12835 ns/op

想像どおりセマフォ方式の方がオーバーヘッドが大きいようです。
……が、結局のトコ無視できるくらいですね。

そんなわけでよっぽどパフォーマンス重視のトコ以外は可読性で判断すればいいという結論なんですけど、みなさん、どちらがお好みでしょうか?

contextの揮発性について

えーっと、そもそもなんですけど、contextって揮発性が求められるんですっけ???
極端な例でいうと、context.Background()で帰ってくるcontextなんかは全然揮発しないpackageのvarで定義されちゃってますけど……

さてさて、contextの用途的に、

  • Dispatcher全体のcontext
  • その処理の一部であるjobの処理のcontext

という流れで派生関係があるというのは、個人的には違和感ないですし、その派生関係を利用してDispacher全体をキャンセルさせるStopImmediately()みたいな実装もできたわけで……
# というかあの例はStopImmediately()のためにcontext対応したようなもんですケド(;・∀・)

んでもって、Start/StopみたいにライフサイクルがはっきりしているDispatcherのようなモノのcontextは、自身で(ラップして)管理させるほうがキレイなんじゃないかなぁと思います。


とはいえこういう規約もあるわけで、判断に悩むトコではあるんですけどね……

// Do not store Contexts inside a struct type; instead, pass a Context
// explicitly to each function that needs it. The Context should be the first
// parameter, typically named ctx:
//
//  func DoSomething(ctx context.Context, arg Arg) error {
//    // ... use ctx ...
//  }

でもこれはfunctionで使うcontextの渡し方に限定した内容だと思えば、Dispatcherが内部にcontextを保持するのは矛盾しないかなぁ……と思ってみたり。

い、いかがでしょうか???

……やっぱり、struct typeでラップするのは、、、ダメですかね???