1 ポイント 投稿者 GN⁺ 2024-08-29 | 1件のコメント | WhatsAppで共有

はじめに

  • 私たちは、世界初のバージョン管理SQLデータベースであるDoltをGo言語で開発している
  • ほとんどのGoコードベースと同様に、チャネルとゴルーチンを使って並行実行を実装している
  • 一般に並行プログラミングは難しいため、シンプルで直感的な方法を使っている
  • しかし、別のオープンソースプロジェクトから、チャネルを非常に独創的に使うコードを引き継いだ
var c chan chan struct{}
  • これは、異なるゴルーチン間でチャネルを受け渡すことで、ワーカーゴルーチン間のファンアウトパターンを実装するものだった
  • この方式は理解しにくく、ゴルーチンリークを考えると扱いづらかった
  • 最終的にこのコードを書き直し、chan chan struct{}を取り除いた

なぜこんなことをするのか

  • C言語とその派生言語が支配的だった時代からの古いプログラミングジョークがある
  • 多くの人がポインタを理解するのに苦労していた
  • GoもCから派生した言語なので、同じことができる
func main() {
  i := 1
  setInt(&i)
  fmt.Printf("i is now %d", i)
}

func setInt(i *int) {
  setInt2(&i)
}

func setInt2(i **int) {
  setInt3(&i)
}

func setInt3(i ***int) {
  setInt4(&i)
}

func setInt4(i ****int) {
  ****i = 100
}
  • このコードはコンパイルされ、i is now 100を出力する
  • Goではチャネルを使って同じことができる

4-chan Goプログラマー

  • 4段階のチャネル間接参照を使うプログラムを書く
  • 最上位チャネルは4-chanとして宣言される
_4chan := make(chan chan chan chan int)
  • このチャネルに送る値は3-chanである
_3chan := make(chan chan chan int)
  • 各間接参照の段階で、一定の分岐係数に応じてプロデューサーを生成する
func sendChanChanChan(c chan chan chan chan int) {
  for range factor {
    go func() {
      logrus.Debug("starting 3chan producer")
      _3chan := make(chan chan chan int)
      sendChanChan(c, _3chan)
    }()
  }
}
  • コンシューマーも同様に処理する
func receiveChanChanChan(c chan chan chan chan int) {
  for _3chan := range c {
    logrus.Debug("got message from 4chan")
    for range factor {
      logrus.Debug("starting 3chan consumer")
      go receiveChanChan(_3chan)
    }
  }
}
  • 最後に実際の値を送る段階に到達する
func send(_2chan chan chan int, _1chan chan int) {
  _2chan <- _1chan
  for range factor {
    go func() {
      logrus.Debug("starting int producer")
      for range factor {
        go func() {
          logrus.Debug("sending int")
          _1chan <- 1
        }()
      }
    }()
  }
}
  • コンシューマーは受け取った値を合計する
var sum = &atomic.Int32{}

func receive(c chan int) {
  for s := range c {
    logrus.Debug("received int")
    sum.Add(int32(s))
  }
}
  • すべてをまとめて実行する
const factor = 3
var sum = &atomic.Int32{}

func main() {
  // logrus.SetLevel(logrus.DebugLevel)
  _4chan := make(chan chan chan chan int)
  go sendChanChanChan(_4chan)
  go receiveChanChanChan(_4chan)
  time.Sleep(500 * time.Millisecond)
  fmt.Printf("%d ^ 5: %d", factor, sum.Load())
}
  • このプログラムは、数値の5乗をできるだけ分散した方法で計算する

論評

  • 実際のコードでこうしてはいけない理由はたくさんある。実装やデバッグの難しさ、自尊心の問題、同僚からの非難などだ
  • しかし、とても面白く、しかも動くという点で興味深い
  • 実用的な理由の1つとして、チャネルをチャネルに送るとクローズが非常に難しくなる

結論

  • Goにおける面白い並行性パターンについて質問や意見があれば、Discordで私たちのチームや他のDoltユーザーと話すことができる

GN⁺のまとめ

  • この記事は、Go言語でチャネルを使った独創的な並行性パターンを扱っている
  • 実際のコードで使うには非効率だが、概念的には興味深い
  • DoltのようなプロジェクトでGoの並行性機能をどのように活用できるかを示している
  • 類似の機能を持つプロジェクトとしては、PostgreSQLやMySQLなどがある

1件のコメント

 
GN⁺ 2024-08-29
Hacker Newsの意見
  • 科学者としてプロのソフトウェアエンジニアと一緒に働くと、彼らがやっていることの多くが理解できない

    • 1行のコードが4つの「インターフェース関数」を経由して呼び出されるのを見たことがある
    • 各関数は別々のファイルやフォルダにあり、コードを読むのがとても疲れる
    • 数段階入り込んだら、実際に計算している部分にたどり着けるのか疑問に思う
  • あまり労力のかかっていない中身の薄いコメントを残したい

    • 最初の数段落のミームは、Cプログラマとして笑えた
    • 言語の奇妙な変種を見るのが好きで、Goでそれを見るのが興味深い
  • Cとその派生言語が支配していた時代の古いプログラミングジョークが、今でも通用する

  • Buena Vista Social Clubのクラシック音楽を思い出す

  • 特定の状況で「chan chan Value」または「chan struct{resp chan Value}」パターンを使ったことがある

    • メッセージバスを代わりに使うこともできたが、そうするとメッセージバスを処理しなければならない状況になる
  • チャネルのチャネルは一般的なパターンで、通常は構造体型のフィールドがチャネルという形で現れる

    • リクエストを送り、ワーカーが作業を完了した後に結果を応答チャネルへ入れる方式
    • type request struct { params, reply chan response } のような形
    • 2つのチャネルは有用で、3つ以上のチャネルは見たことがない
  • 動的ディスパッチ機構を実装するためにチャネルを使うことへの反対意見のブログ

  • Joe Armstrongの「My favorite Erlang Program」を思い出させる

  • リンクをクリックしたとき、別のものを期待していた

    • Goプログラマではないので、ジョークにすぐには気づかなかった
  • LabVIEWコードで非同期の応答データを受け取るために似たような方法を使う

    • 応答をキューにダンプする代わりに、コールバックイベントチャネルを含むメッセージを渡す
    • メモリの無駄はあるが、単回使用後に応答時に閉じられるので効率的だ