Upgrade to Pro — share decks privately, control downloads, hide ads and more …

なぜselectはselectではないのか

鹿
March 19, 2025

 なぜselectはselectではないのか

鹿

March 19, 2025
Tweet

More Decks by 鹿

Other Decks in Programming

Transcript

  1. Go の select • statement (文) • https://go.dev/ref/spec#Select_statements • >

    A “select” statement chooses which of a set of possible send or receive operations will proceed. • send と receive ができているものを選択する(順序保証は無し) • "A Tour of Go" では Concurrency で2番目に出てくるぐらい重要
  2. func main() { tick := time.Tick(100 * time.Millisecond) boom :=

    time.After(500 * time.Millisecond) for { select { case <-tick: fmt.Println("tick.") case <-boom: fmt.Println("BOOM!") return default: fmt.Println(" .") time.Sleep(50 * time.Millisecond) } } }
  3. select…聞いたことがある… • https://en.wikipedia.org/wiki/Select(Unix) • POSIX で規定されている、UNIX OS が提供する system call

    • > ... for examining the status of file descriptors of open input/output channels. • ここで書いている "channel" は抽象的な意味の用語で、Go のチャネルではない。 それより注目すべきは "file descriptors" のところ。 • UNIX で fd (file descriptor) といえば、そう、ファイルとソケット。 fd に対する操作が read と wite。 • つまり UNIX の select system call は read/write を多重化してくれる。 少なくともどれか一つが read もしくは write 可能になるのを待って、それを教える。 select の後に教えてもらった fd を read もしくは write する。
  4. int foo() { fd_set readfds; struct timeval tv; int ret;

    FD_ZERO(&readfds); FD_SET(STDIN_FILENO, &readfds); //STDINを監視する tv.tv_sec = 5; // 待ち時間 tv.tv_usec = 0; ret = select(STDIN_FILENO + 1, &readfds, NULL, NULL, &tv); //★ if (ret == -1) { /*エラー処理*/ } else if (ret == 0) { /*タイムアウト*/ } else { if (FD_ISSET(STDIN_FILENO, &readfds)) {/*STDINを読む*/ } } }
  5. Go の select の話に戻ると • > A "select" statement chooses

    which of a set of possible send or receive operations will proceed. • “send” は `<-var`、“receive” は `var<-` のこと • これは io.Reader/io.Writer じゃなくてチャネル操作 • あれ? Read/Write を多重化できないの? → できない!なんで? • できたら便利なのに…
  6. channelとioの多重化が同時にできれば良いのに • たとえば「Read しつつ周期的に Keep-Alive Probe を送りたい」みたいな 処理をシンプルに書ける ticker :=

    time.NewTicker(time.Minute) defer ticker.Stop() for { select { case <-ticker.C: // send keep-alive probe case n, err := r.Read(buf): // handle read data } }
  7. 実は危険な書き方 func ReadWithContext(ctx context.Context, conn net.Conn, buffer []byte) (n int,

    err error) { done := make(chan struct{}) go func() { n, err = conn.Read(buffer) close(done) }() select { case <-ctx.Done(): return 0, ctx.Err() case <-done: return n, err }
  8. 実は危険な書き方 • これ良く見ると、 Read の goroutine が置き去り • 確実に殺す方法が無い •

    ReadWithContext() を2回呼ぶと2箇所でReadすることになり、破綻する • これを回避しようとすると一気に面倒になる
  9. なぜ select なのに select じゃないの? • AI に聞いてみた…んだけど納得できない回答しか出なかった 1.select はchannel

    を使った並行処理のために設計されていて、 io.Reader/Writer とは抽象化レベルが違うから →いやどちらも入出力を抽象化しているのでは? 2.io.Reader/Writer はブロッキング処理なので select で待機できない →いや UNIX の select みたいに「入力があったら返す」ってしたら良いじゃん 3.select をチャネル操作に特化することでコードがシンプルになり Go の哲学に合う →哲学を言われたらもう何も言えん • うーん納得しがたい
  10. • select が file descriptor を対象にしてないの変じゃない? Go には file descriptor

    をポーリングする手段 が無い。Read も中断できないし。 • Read/Write はブロッキングだから、ノンブロッキングで扱いたければgoroutineを作ればいいじゃん。 それでシンプルじゃん。 • いや、Readを中断じゃなくて、selectを中断と書きたかった。Cとかだとプロセス内でpipe使って信号 送って、selectで多重処理できるじゃん。 • select で Read してる人とタイムアウトを両方待てばいいじゃん。 • select はチャネルしか待てないし、goroutine でRead してる人はタイムアウトになっても Read を止めないよね? • もう Go の select じゃなくて select(2) system call を使いなよ。 • Read してる goroutine を安全に回収するすべが無いのをみんなちゃんと考えるべきだ。 Erlang みたいな監視ツリーを作れば(以下脱線 • Rust で Tokio を使う場合、ノンブロッキングI/O を futures (JS の Promise みたいなの) で書く。 それがステートマシンにコンパイルされてシングルスレッドで kqueue/epoll を使ったイベントループに なって… • それ Nim に似てるね。async await, async dispatcher, selector があって… • やはり Concurrent ML が今でもノンブロッキングI/Oとイベントハンドリングの最適解だよ。 • 「Go には file descriptor をポーリングする手段が無い」あるよ。自分で select/epoll システムコール を呼べばいいよ。OSごとの違いを自分で吸収するのは大変だけど。でも Go が select をすべての非同 期操作の窓口にしないのは私もどうかと思う。CSP 的にも良いのに。 • …以下続く…
  11. まとめ • Go の select と select system call は名前は同じだけどできることが違う

    • Go の select が io.Reader/Writer を対象にしていないのは、 おそらく設計やコードをシンプルにしたいからだろう • ただしそれに納得が行かない人もいるし、ReadWithContext や ReadWithTimeout を安全に書く方法も提供されていない • どうしてもやりたい人は select system call を直接呼び出してなんとかしよう (ただしクロスプラットフォームが大変)