はじめに
- 私たちは、世界初のバージョン管理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)
_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件のコメント
Hacker Newsの意見
科学者としてプロのソフトウェアエンジニアと一緒に働くと、彼らがやっていることの多くが理解できない
あまり労力のかかっていない中身の薄いコメントを残したい
Cとその派生言語が支配していた時代の古いプログラミングジョークが、今でも通用する
Buena Vista Social Clubのクラシック音楽を思い出す
特定の状況で「chan chan Value」または「chan struct{resp chan Value}」パターンを使ったことがある
チャネルのチャネルは一般的なパターンで、通常は構造体型のフィールドがチャネルという形で現れる
type request struct { params, reply chan response }のような形動的ディスパッチ機構を実装するためにチャネルを使うことへの反対意見のブログ
Joe Armstrongの「My favorite Erlang Program」を思い出させる
リンクをクリックしたとき、別のものを期待していた
LabVIEWコードで非同期の応答データを受け取るために似たような方法を使う