「Go言語による並行処理」の気になった所まとめ3 orチャネルとor-done チャネル

or チャネル

任意の数のチャンネルを待ちたいときに作るパターンです。
サンプルでは引数が0個のときにnilを返します。最初、再起だからなのかと思ったのですが、再起では必ず1個以上の引数になります。初回に0個の場合だけなので、0個の引数は取らないほうがいいような気がします。3.3章でnilチャンネルは読み込みも書き込みもブロックしてcloseはpanicになると書いていたのに...

ただ、このパターンを実装した場合、可読性が低いのでこの本を読んでいない人には何これって言われそうです...

package main

import (
  "fmt"
  "time"
)

// この関数はチャネルの可変長引数のスライスを受け取り、1つのチャネルを返す
func or(channels ...<-chan interface{}) <-chan interface{} {
  // 再帰関数なので、停止条件が必要
  // スライスが空の場合は単純にnilチャネルを返す。チャネルを渡さなかった場合と同義
  // 必ずorDoneは渡されるので、再起中は引数が0はありえない
  switch len(channels) {
  case 0:
    return nil
  case 1:
    // 可変長引数のスライスが1つしか要素を持っていない場合は、要素を返すだけ
    return channels[0]
  }
  orDone := make(chan interface{})
  // 関数の本体で再帰が発生する部分
  // ゴルーチンを作り、ブロックすることなく作ったチャネルにメッセージを受け取れるようする
  go func() {
    defer close(orDone)

    switch len(channels) {
    // orへの各再帰呼出しは少なくとも2つのチャネルを持っているので
    // ゴルーチンの数を制限するため、特別な条件を設定
    case 2:
      select {
      case <-channels[0]:
      case <-channels[1]:
      }
    default:
      // スライスの3番目以降のチャネルから再帰的にorチャネルを作成して、
      // そこからselectを行う
      // この再帰関係はスライスの残りの部分をorチャネルに分解して、
      // 最初のシグナルが返ってくる木構造を形成する
      // orDoneチャネルも渡して、木構造の上位の部分が終了したら
      // 下位の部分も終了するようにする
      select {
      case <-channels[0]:
      case <-channels[1]:
      case <-channels[2]:
      // 再起の場合、チャンネルは最低1は渡す
      case <-or(append(channels[3:], orDone)...):
      }
    }
  }()
  return orDone
}

//試す用のチャンネルを生成する関数
func sig(after time.Duration) <-chan interface{} {
  c := make(chan interface{})
  go func() {
    defer close(c)
    time.Sleep(after)
  }()
  return c
}

func main() {
  start := time.Now()
  // 最も早い1秒後に終わる
  <-or(
    sig(1*time.Second),
    sig(2*time.Hour),
    sig(5*time.Minute),
    sig(1*time.Hour),
  )
  fmt.Printf("done after %v", time.Since(start))
}

単純にキャンセルしたいだけならcontextパッケージを使ったほうがよいですね。本にも書いてます。
5.6 不健全なゴルーチンを直すのサンプルでorパターンが出てくるので書きました。

or-done チャネル

Goを書いていると目的のチャンネルとdoneチャンネルの最低2つselectする場合がよくあります。その場合、下記のようになります。

  for val := range myChan {
    // valに対して何かする
  }

// 次のように膨れ上がる

loop:
  for {
    select {
    case <-done:
      break loop
    case maybeVal, ok := <-myChan:
      if ok == false {
        return // あるいはforからbreakするとか
      }
      // valに対して何かする
    }
  }

上のイディオムをラップしたものがorDoneです。チャンネルが1つ増えているので嫌いな人はいるかも知れません...本では、とりあえず可読性のほうが重要だよね。問題になったら変えればいいじゃん的な雰囲気で推奨(?)しています。

func orDone(done, c <-chan interface{}) <-chan interface{} {
  valStream := make(chan interface{})
  go func() {
    defer close(valStream)
    for {
      select {
      case <-done:
        return
      case v, ok := <-c:
        if ok == false {
          return
        }
        select {
        case valStream <- v:
        case <-done:
        }
      }
    }
  }()
  return valStream
}

// またrangeが使える
for val := range orDone(done, myChan) { 
  // valに対して何かする
} 
``